diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 1912d8ca3..000000000 --- a/.gitmodules +++ /dev/null @@ -1,126 +0,0 @@ -[submodule "apache"] - path = apache - url = https://github.com/puppetlabs/puppetlabs-apache.git -[submodule "ceilometer"] - path = ceilometer - url = https://github.com/stackforge/puppet-ceilometer.git -[submodule "certmonger"] - path = certmonger - url = https://github.com/rcritten/puppet-certmonger.git -[submodule "cinder"] - path = cinder - url = https://github.com/stackforge/puppet-cinder.git -[submodule "concat"] - path = concat - url = https://github.com/puppetlabs/puppetlabs-concat.git -[submodule "firewall"] - path = firewall - url = https://github.com/puppetlabs/puppetlabs-firewall.git -[submodule "glance"] - path = glance - url = https://github.com/stackforge/puppet-glance.git -[submodule "haproxy"] - path = haproxy - url = https://github.com/puppetlabs/puppetlabs-haproxy.git -[submodule "heat"] - path = heat - url = https://github.com/stackforge/puppet-heat.git -[submodule "horizon"] - path = horizon - url = https://github.com/stackforge/puppet-horizon.git -[submodule "inifile"] - path = inifile - url = https://github.com/puppetlabs/puppetlabs-inifile.git -[submodule "memcached"] - path = memcached - url = https://github.com/saz/puppet-memcached.git -[submodule "mongodb"] - path = mongodb - url = https://github.com/puppetlabs/puppetlabs-mongodb.git -[submodule "mysql"] - path = mysql - url = https://github.com/puppetlabs/puppetlabs-mysql.git -[submodule "nova"] - path = nova - url = https://github.com/stackforge/puppet-nova.git -[submodule "nssdb"] - path = nssdb - url = https://github.com/rcritten/puppet-nssdb.git -[submodule "openstack"] - path = openstack - url = https://github.com/stackforge/puppet-openstack.git -[submodule "pacemaker"] - path = pacemaker - url = https://github.com/radez/puppet-pacemaker.git -[submodule "qpid"] - path = qpid - url = https://github.com/dprince/puppet-qpid -[submodule "rsync"] - path = rsync - url = https://github.com/puppetlabs/puppetlabs-rsync.git -[submodule "ssh"] - path = ssh - url = https://github.com/saz/puppet-ssh.git -[submodule "stdlib"] - path = stdlib - url = https://github.com/puppetlabs/puppetlabs-stdlib.git -[submodule "swift"] - path = swift - url = https://github.com/stackforge/puppet-swift.git -[submodule "sysctl"] - path = sysctl - url = https://github.com/puppetlabs/puppetlabs-sysctl.git -[submodule "tempest"] - path = tempest - url = https://github.com/stackforge/puppet-tempest.git -[submodule "vcsrepo"] - path = vcsrepo - url = https://github.com/puppetlabs/puppetlabs-vcsrepo.git -[submodule "vlan"] - path = vlan - url = https://github.com/derekhiggins/puppet-vlan.git -[submodule "vswitch"] - path = vswitch - url = https://github.com/stackforge/puppet-vswitch.git -[submodule "xinetd"] - path = xinetd - url = https://github.com/puppetlabs/puppetlabs-xinetd.git -[submodule "rabbitmq"] - path = rabbitmq - url = https://github.com/puppetlabs/puppetlabs-rabbitmq.git -[submodule "staging"] - path = staging - url = https://github.com/nanliu/puppet-staging.git -[submodule "gluster"] - path = gluster - url = https://github.com/purpleidea/puppet-gluster.git -[submodule "common"] - path = common - url = https://github.com/purpleidea/puppet-common.git -[submodule "puppet"] - path = puppet - url = https://github.com/purpleidea/puppet-puppet.git -[submodule "galera"] - path = galera - url = https://github.com/rohara/puppet-galera.git -[submodule "module-data"] - path = module-data - url = https://github.com/ripienaar/puppet-module-data -[submodule "sahara"] - path = sahara - url = https://github.com/stackforge/puppet-sahara.git -[submodule "keystone"] - path = keystone - url = https://github.com/stackforge/puppet-keystone.git -[submodule "neutron"] - path = neutron - url = https://github.com/stackforge/puppet-neutron.git -[submodule "openstacklib"] - path = openstacklib - url = https://github.com/stackforge/puppet-openstacklib -[submodule "nagios"] - path = nagios - url = https://github.com/gildub/puppet-nagios-openstack.git -[submodule "n1k-vsm"] - path = n1k-vsm - url = https://github.com/stackforge/puppet-n1k-vsm diff --git a/Puppetfile b/Puppetfile new file mode 100644 index 000000000..47eccf7e4 --- /dev/null +++ b/Puppetfile @@ -0,0 +1,167 @@ +mod 'apache', + :git => 'https://github.com/puppetlabs/puppetlabs-apache.git', + :commit => '769ff363a8a3c51e24f63a2494217d2d029289c6' + +mod 'ceilometer', + :git => 'https://github.com/stackforge/puppet-ceilometer.git', + :commit => '08fc9d9159cd9eb0830d550abb1058bc2b9b5759' + +mod 'certmonger', + :git => 'https://github.com/rcritten/puppet-certmonger.git', + :commit => '5fbf10fbbff4aed4db30e839c63c99b195e8425a' + +mod 'cinder', + :git => 'https://github.com/stackforge/puppet-cinder.git', + :commit => '2da616a4a52d3086fe3a291b9199fc7313575504' + +mod 'common', + :git => 'https://github.com/purpleidea/puppet-common.git', + :commit => '2c0ed2844c606fd806bde0c02e47e79c88fab4a9' + +mod 'concat', + :git => 'https://github.com/puppetlabs/puppetlabs-concat.git', + :commit => '07bba0bcad2e3a2baf19dbff8b1a5146d9141153' + +mod 'firewall', + :git => 'https://github.com/puppetlabs/puppetlabs-firewall.git', + :commit => 'd5a10f5a52d84b9fcfb8fc65ef505685a07d5799' + +mod 'galera', + :git => 'https://github.com/rohara/puppet-galera.git', + :commit => 'e35922bbb31ef2e6a86c7973cbafea96a8b160af' + +mod 'glance', + :git => 'https://github.com/stackforge/puppet-glance.git', + :commit => 'f377c0229c006b02f43a14be4979553e983cb98e' + +mod 'gluster', + :git => 'https://github.com/purpleidea/puppet-gluster.git', + :commit => '6c962083d8b100dcaeb6f11dbe61e6071f3d13f0' + +mod 'haproxy', + :git => 'https://github.com/puppetlabs/puppetlabs-haproxy.git', + :commit => 'f381510e940ee11feb044c1c728ba2e5af807c79' + +mod 'heat', + :git => 'https://github.com/stackforge/puppet-heat.git', + :commit => 'e9e1ba05e13948b8e0c7a72b1b68cefbedd2b40d' + +mod 'horizon', + :git => 'https://github.com/stackforge/puppet-horizon.git', + :commit => '16b482ea21a70d8dd06ab4c98ac5a218399b0213' + +mod 'inifile', + :git => 'https://github.com/puppetlabs/puppetlabs-inifile.git', + :commit => 'fe9b0d5229ea37179a08c4b49239da9bc950acd1' + +mod 'keystone', + :git => 'https://github.com/stackforge/puppet-keystone.git', + :commit => '605161f3d4b7bbcffc657c86b367159701dfdcbe' + +mod 'memcached', + :git => 'https://github.com/saz/puppet-memcached.git', + :commit => '49dbf102fb6eee90297b2ed6a1fa463a8c5ccee7' + +mod 'module-data', + :git => 'https://github.com/ripienaar/puppet-module-data.git', + :commit => '159fc5e0e21ce9df96c777f0064b5eca88e29cae' + +mod 'mongodb', + :git => 'https://github.com/puppetlabs/puppetlabs-mongodb.git', + :commit => '0518f864afcce2ebb79f1f2edab5de323c811af7' + +mod 'mysql', + :git => 'https://github.com/puppetlabs/puppetlabs-mysql.git', + :commit => 'c70fc13fc15740b61b8eccd3c79168d3e417a374' + +mod 'n1k-vsm', + :git => 'https://github.com/stackforge/puppet-n1k-vsm.git', + :commit => '69ff094069506f98431182c6097b3b6b9ea6fdb9' + +mod 'nagios', + :git => 'https://github.com/gildub/puppet-nagios-openstack.git', + :commit => '56a1eee350c4600bb12e017d64238fb3f876abd4' + +mod 'neutron', + :git => 'https://github.com/stackforge/puppet-neutron.git', + :commit => 'dcd122e477713421d9601d93d13725a4871b9c42' + +mod 'nova', + :git => 'https://github.com/stackforge/puppet-nova.git', + :commit => 'a79e5338df5f85cb299183e54b39e8a22a640f59' + +mod 'nssdb', + :git => 'https://github.com/rcritten/puppet-nssdb.git', + :commit => 'b3799a9a7c62c3b5b7968f9860220a885b45fb8a' + +mod 'openstack', + :git => 'https://github.com/stackforge/puppet-openstack.git', + :commit => 'd81d2d86280d5739cc896a48b68d7309e765047a' + +mod 'openstacklib', + :git => 'https://github.com/stackforge/puppet-openstacklib.git', + :commit => 'c374bed10f8af6000601fa407ebaef0833e1999c' + +mod 'pacemaker', + :git => 'https://github.com/radez/puppet-pacemaker.git', + :commit => '0ed9ee8a29c0f27e86727d415b39d2715332df7d' + +mod 'puppet', + :git => 'https://github.com/purpleidea/puppet-puppet.git', + :commit => 'bd467cae15eba9ca44274034d2593b0eaf30518d' + +mod 'qpid', + :git => 'https://github.com/dprince/puppet-qpid', + :commit => '1f0c32b39ad17e7acbd440b50fb6f0875971f5e1' + +mod 'rabbitmq', + :git => 'https://github.com/puppetlabs/puppetlabs-rabbitmq.git', + :commit => 'cbda1ced336f9768ebd442415b4d9c7c4ddb48c7' + +mod 'rsync', + :git => 'https://github.com/puppetlabs/puppetlabs-rsync.git', + :commit => '357d51f3a6a22bc3da842736176c3510e507b4fb' + +mod 'sahara', + :git => 'https://github.com/stackforge/puppet-sahara.git', + :commit => 'f4e5681cfb289113be1ba49c12709145ecbad938' + +mod 'ssh', + :git => 'https://github.com/saz/puppet-ssh.git', + :commit => 'd6571f8c43ac55d20a6afd8a8ce3f86ac4b0d7a4' + +mod 'staging', + :git => 'https://github.com/nanliu/puppet-staging.git', + :commit => '887275d8fb20e148c6f9eb327f1f6c8ea5ee280f' + +mod 'stdlib', + :git => 'https://github.com/puppetlabs/puppetlabs-stdlib.git', + :commit => '62e8c1d76902e6f22cb9f7b3abd43e757b4130a3' + +mod 'swift', + :git => 'https://github.com/stackforge/puppet-swift.git', + :commit => '3ea00440361ff2452561d2cce808d938e39cce56' + +mod 'sysctl', + :git => 'https://github.com/puppetlabs/puppetlabs-sysctl.git', + :commit => 'c4486acc2d66de857dbccd8b4b945ea803226705' + +mod 'tempest', + :git => 'https://github.com/stackforge/puppet-tempest.git', + :commit => '7a3369949fc8af41e190dd8115391354a7575ecb' + +mod 'vcsrepo', + :git => 'https://github.com/puppetlabs/puppetlabs-vcsrepo.git', + :commit => '6f7507a2a48ff0a58c7db026760a2eb84e382a77' + +mod 'vlan', + :git => 'https://github.com/derekhiggins/puppet-vlan.git', + :commit => 'c937de75c28e63fba8d8738ad6a5f2ede517e53d' + +mod 'vswitch', + :git => 'https://github.com/stackforge/puppet-vswitch.git', + :commit => '17b62e56e07eeed25fd2aaef278a16c97155a115' + +mod 'xinetd', + :git => 'https://github.com/puppetlabs/puppetlabs-xinetd.git', + :commit => '6b02de8d4f30a819eb404048e4258e3a5e8023c8' diff --git a/apache b/apache deleted file mode 160000 index 769ff363a..000000000 --- a/apache +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 769ff363a8a3c51e24f63a2494217d2d029289c6 diff --git a/apache/.fixtures.yml b/apache/.fixtures.yml new file mode 100644 index 000000000..b5f76c03a --- /dev/null +++ b/apache/.fixtures.yml @@ -0,0 +1,6 @@ +fixtures: + repositories: + stdlib: "git://github.com/puppetlabs/puppetlabs-stdlib.git" + concat: "git://github.com/puppetlabs/puppetlabs-concat.git" + symlinks: + apache: "#{source_dir}" diff --git a/apache/.gitignore b/apache/.gitignore new file mode 100644 index 000000000..e8f52b445 --- /dev/null +++ b/apache/.gitignore @@ -0,0 +1,7 @@ +.pkg +Gemfile.lock +vendor +spec/fixtures +.rspec_system +.bundle +.*sw* diff --git a/apache/.nodeset.yml b/apache/.nodeset.yml new file mode 100644 index 000000000..767f9cd2f --- /dev/null +++ b/apache/.nodeset.yml @@ -0,0 +1,31 @@ +--- +default_set: 'centos-64-x64' +sets: + 'centos-59-x64': + nodes: + "main.foo.vm": + prefab: 'centos-59-x64' + 'centos-64-x64': + nodes: + "main.foo.vm": + prefab: 'centos-64-x64' + 'fedora-18-x64': + nodes: + "main.foo.vm": + prefab: 'fedora-18-x64' + 'debian-607-x64': + nodes: + "main.foo.vm": + prefab: 'debian-607-x64' + 'debian-70rc1-x64': + nodes: + "main.foo.vm": + prefab: 'debian-70rc1-x64' + 'ubuntu-server-10044-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-10044-x64' + 'ubuntu-server-12042-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-12042-x64' diff --git a/apache/.puppet-lint.rc b/apache/.puppet-lint.rc new file mode 100644 index 000000000..df733ca81 --- /dev/null +++ b/apache/.puppet-lint.rc @@ -0,0 +1,5 @@ +--no-single_quote_string_with_variables-check +--no-80chars-check +--no-class_inherits_from_params_class-check +--no-class_parameter_defaults-check +--no-documentation-check diff --git a/apache/.travis.yml b/apache/.travis.yml new file mode 100644 index 000000000..5efc64fa7 --- /dev/null +++ b/apache/.travis.yml @@ -0,0 +1,40 @@ +--- +branches: + only: + - master +language: ruby +bundler_args: --without development +script: "bundle exec rake spec SPEC_OPTS='--format documentation'" +after_success: + - git clone -q git://github.com/puppetlabs/ghpublisher.git .forge-releng + - .forge-releng/publish +rvm: + - 1.8.7 + - 1.9.3 + - 2.0.0 +env: + matrix: + - PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.6.0" + - PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.7.0" + - PUPPET_GEM_VERSION="~> 3.0" + global: + - PUBLISHER_LOGIN=puppetlabs + - secure: |- + MO4pB4bqBQJjm2yFHf3Mgho+y0Qv4GmMxTMhzI02tGy1V0HMtruZbR7EBN0i + n2CiR7V9V0mNR7/ymzDMF9yVBcgqyXMsp/C6u992Dd0U63ZwFpbRWkxuAeEY + ioupWBkiczjVEo+sxn+gVOnx28pcH/X8kDWbr6wFOMIjO03K66Y= +matrix: + fast_finish: true + exclude: + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.6.0" + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.7.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.6.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.7.0" + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 3.2.0" +notifications: + email: false diff --git a/apache/CHANGELOG.md b/apache/CHANGELOG.md new file mode 100644 index 000000000..57d62e4c1 --- /dev/null +++ b/apache/CHANGELOG.md @@ -0,0 +1,209 @@ +## 2014-01-31 Release 0.11.0 +### Summary: + +This release adds preliminary support for Windows compatibility and multiple rewrite support. + +### Backwards-incompatible Changes: + +- The rewrite_rule parameter is deprecated in favor of the new rewrite parameter + and will be removed in a future release. + +### Features: + +- add Match directive +- quote paths for windows compatibility +- add auth_group_file option to README.md +- allow AuthGroupFile directive for vhosts +- Support Header directives in vhost context +- Don't purge mods-available dir when separate enable dir is used +- Fix the servername used in log file name +- Added support for mod_include +- Remove index parameters. +- Support environment variable control for CustomLog +- added redirectmatch support +- Setting up the ability to do multiple rewrites and conditions. +- Convert spec tests to beaker. +- Support php_admin_(flag|value)s + +### Bugfixes: + +- directories are either a Hash or an Array of Hashes +- Configure Passenger in separate .conf file on RH so PassengerRoot isn't lost +- (docs) Update list of `apache::mod::[name]` classes +- (docs) Fix apache::namevirtualhost example call style +- Fix $ports_file reference in apache::listen. +- Fix $ports_file reference in Namevirtualhost. + + +## 2013-12-05 Release 0.10.0 +### Summary: + +This release adds FreeBSD osfamily support and various other improvements to some mods. + +### Features: + +- Add suPHP_UserGroup directive to directory context +- Add support for ScriptAliasMatch directives +- Set SSLOptions StdEnvVars in server context +- No implicit entry for ScriptAlias path +- Add support for overriding ErrorDocument +- Add support for AliasMatch directives +- Disable default "allow from all" in vhost-directories +- Add WSGIPythonPath as an optional parameter to mod_wsgi. +- Add mod_rpaf support +- Add directives: IndexOptions, IndexOrderDefault +- Add ability to include additional external configurations in vhost +- need to use the provider variable not the provider key value from the directory hash for matches +- Support for FreeBSD and few other features +- Add new params to apache::mod::mime class +- Allow apache::mod to specify module id and path +- added $server_root parameter +- Add Allow and ExtendedStatus support to mod_status +- Expand vhost/_directories.pp directive support +- Add initial support for nss module (no directives in vhost template yet) +- added peruser and event mpms +- added $service_name parameter +- add parameter for TraceEnable +- Make LogLevel configurable for server and vhost +- Add documentation about $ip +- Add ability to pass ip (instead of wildcard) in default vhost files + +### Bugfixes: + +- Don't listen on port or set NameVirtualHost for non-existent vhost +- only apply Directory defaults when provider is a directory +- Working mod_authnz_ldap support on Debian/Ubuntu + +## 2013-09-06 Release 0.9.0 +### Summary: +This release adds more parameters to the base apache class and apache defined +resource to make the module more flexible. It also adds or enhances SuPHP, +WSGI, and Passenger mod support, and support for the ITK mpm module. + +### Backwards-incompatible Changes: +- Remove many default mods that are not normally needed. +- Remove `rewrite_base` `apache::vhost` parameter; did not work anyway. +- Specify dependencies on stdlib >=2.4.0 (this was already the case, but +making explicit) +- Deprecate `a2mod` in favor of the `apache::mod::*` classes and `apache::mod` +defined resource. + +### Features: +- `apache` class + - Add `httpd_dir` parameter to change the location of the configuration + files. + - Add `logroot` parameter to change the logroot + - Add `ports_file` parameter to changes the `ports.conf` file location + - Add `keepalive` parameter to enable persistent connections + - Add `keepalive_timeout` parameter to change the timeout + - Update `default_mods` to be able to take an array of mods to enable. +- `apache::vhost` + - Add `wsgi_daemon_process`, `wsgi_daemon_process_options`, + `wsgi_process_group`, and `wsgi_script_aliases` parameters for per-vhost + WSGI configuration. + - Add `access_log_syslog` parameter to enable syslogging. + - Add `error_log_syslog` parameter to enable syslogging of errors. + - Add `directories` hash parameter. Please see README for documentation. + - Add `sslproxyengine` parameter to enable SSLProxyEngine + - Add `suphp_addhandler`, `suphp_engine`, and `suphp_configpath` for + configuring SuPHP. + - Add `custom_fragment` parameter to allow for arbitrary apache + configuration injection. (Feature pull requests are prefered over using + this, but it is available in a pinch.) +- Add `apache::mod::suphp` class for configuring SuPHP. +- Add `apache::mod::itk` class for configuring ITK mpm module. +- Update `apache::mod::wsgi` class for global WSGI configuration with +`wsgi_socket_prefix` and `wsgi_python_home` parameters. +- Add README.passenger.md to document the `apache::mod::passenger` usage. +Added `passenger_high_performance`, `passenger_pool_idle_time`, +`passenger_max_requests`, `passenger_stat_throttle_rate`, `rack_autodetect`, +and `rails_autodetect` parameters. +- Separate the httpd service resource into a new `apache::service` class for +dependency chaining of `Class['apache'] -> ~> +Class['apache::service']` +- Added `apache::mod::proxy_balancer` class for `apache::balancer` + +### Bugfixes: +- Change dependency to puppetlabs-concat +- Fix ruby 1.9 bug for `a2mod` +- Change servername to be `$::hostname` if there is no `$::fqdn` +- Make `/etc/ssl/certs` the default ssl certs directory for RedHat non-5. +- Make `php` the default php package for RedHat non-5. +- Made `aliases` able to take a single alias hash instead of requiring an +array. + +## 2013-07-26 Release 0.8.1 +### Bugfixes: +- Update `apache::mpm_module` detection for worker/prefork +- Update `apache::mod::cgi` and `apache::mod::cgid` detection for +worker/prefork + +## 2013-07-16 Release 0.8.0 +### Features: +- Add `servername` parameter to `apache` class +- Add `proxy_set` parameter to `apache::balancer` define + +### Bugfixes: +- Fix ordering for multiple `apache::balancer` clusters +- Fix symlinking for sites-available on Debian-based OSs +- Fix dependency ordering for recursive confdir management +- Fix `apache::mod::*` to notify the service on config change +- Documentation updates + +## 2013-07-09 Release 0.7.0 +### Changes: +- Essentially rewrite the module -- too many to list +- `apache::vhost` has many abilities -- see README.md for details +- `apache::mod::*` classes provide httpd mod-loading capabilities +- `apache` base class is much more configurable + +### Bugfixes: +- Many. And many more to come + +## 2013-03-2 Release 0.6.0 +- update travis tests (add more supported versions) +- add access log_parameter +- make purging of vhost dir configurable + +## 2012-08-24 Release 0.4.0 +### Changes: +- `include apache` is now required when using `apache::mod::*` + +### Bugfixes: +- Fix syntax for validate_re +- Fix formatting in vhost template +- Fix spec tests such that they pass + + 2012-05-08 Puppet Labs - 0.0.4 + e62e362 Fix broken tests for ssl, vhost, vhost::* + 42c6363 Changes to match style guide and pass puppet-lint without error + 42bc8ba changed name => path for file resources in order to name namevar by it's name + 72e13de One end too much + 0739641 style guide fixes: 'true' <> true, $operatingsystem needs to be $::operatingsystem, etc. + 273f94d fix tests + a35ede5 (#13860) Make a2enmod/a2dismo commands optional + 98d774e (#13860) Autorequire Package['httpd'] + 05fcec5 (#13073) Add missing puppet spec tests + 541afda (#6899) Remove virtual a2mod definition + 976cb69 (#13072) Move mod python and wsgi package names to params + 323915a (#13060) Add .gitignore to repo + fdf40af (#13060) Remove pkg directory from source tree + fd90015 Add LICENSE file and update the ModuleFile + d3d0d23 Re-enable local php class + d7516c7 Make management of firewalls configurable for vhosts + 60f83ba Explicitly lookup scope of apache_name in templates. + f4d287f (#12581) Add explicit ordering for vdir directory + 88a2ac6 (#11706) puppetlabs-apache depends on puppetlabs-firewall + a776a8b (#11071) Fix to work with latest firewall module + 2b79e8b (#11070) Add support for Scientific Linux + 405b3e9 Fix for a2mod + 57b9048 Commit apache::vhost::redirect Manifest + 8862d01 Commit apache::vhost::proxy Manifest + d5c1fd0 Commit apache::mod::wsgi Manifest + a825ac7 Commit apache::mod::python Manifest + b77062f Commit Templates + 9a51b4a Vhost File Declarations + 6cf7312 Defaults for Parameters + 6a5b11a Ensure installed + f672e46 a2mod fix + 8a56ee9 add pthon support to apache diff --git a/apache/CONTRIBUTING.md b/apache/CONTRIBUTING.md new file mode 100644 index 000000000..e1288478a --- /dev/null +++ b/apache/CONTRIBUTING.md @@ -0,0 +1,234 @@ +Checklist (and a short version for the impatient) +================================================= + + * Commits: + + - Make commits of logical units. + + - Check for unnecessary whitespace with "git diff --check" before + committing. + + - Commit using Unix line endings (check the settings around "crlf" in + git-config(1)). + + - Do not check in commented out code or unneeded files. + + - The first line of the commit message should be a short + description (50 characters is the soft limit, excluding ticket + number(s)), and should skip the full stop. + + - Associate the issue in the message. The first line should include + the issue number in the form "(#XXXX) Rest of message". + + - The body should provide a meaningful commit message, which: + + - uses the imperative, present tense: "change", not "changed" or + "changes". + + - includes motivation for the change, and contrasts its + implementation with the previous behavior. + + - Make sure that you have tests for the bug you are fixing, or + feature you are adding. + + - Make sure the test suites passes after your commit: + `bundle exec rspec spec/acceptance` More information on [testing](#Testing) below + + - When introducing a new feature, make sure it is properly + documented in the README.md + + * Submission: + + * Pre-requisites: + + - Sign the [Contributor License Agreement](https://cla.puppetlabs.com/) + + - Make sure you have a [GitHub account](https://github.com/join) + + - [Create a ticket](http://projects.puppetlabs.com/projects/modules/issues/new), or [watch the ticket](http://projects.puppetlabs.com/projects/modules/issues) you are patching for. + + * Preferred method: + + - Fork the repository on GitHub. + + - Push your changes to a topic branch in your fork of the + repository. (the format ticket/1234-short_description_of_change is + usually preferred for this project). + + - Submit a pull request to the repository in the puppetlabs + organization. + +The long version +================ + + 1. Make separate commits for logically separate changes. + + Please break your commits down into logically consistent units + which include new or changed tests relevant to the rest of the + change. The goal of doing this is to make the diff easier to + read for whoever is reviewing your code. In general, the easier + your diff is to read, the more likely someone will be happy to + review it and get it into the code base. + + If you are going to refactor a piece of code, please do so as a + separate commit from your feature or bug fix changes. + + We also really appreciate changes that include tests to make + sure the bug is not re-introduced, and that the feature is not + accidentally broken. + + Describe the technical detail of the change(s). If your + description starts to get too long, that is a good sign that you + probably need to split up your commit into more finely grained + pieces. + + Commits which plainly describe the things which help + reviewers check the patch and future developers understand the + code are much more likely to be merged in with a minimum of + bike-shedding or requested changes. Ideally, the commit message + would include information, and be in a form suitable for + inclusion in the release notes for the version of Puppet that + includes them. + + Please also check that you are not introducing any trailing + whitespace or other "whitespace errors". You can do this by + running "git diff --check" on your changes before you commit. + + 2. Sign the Contributor License Agreement + + Before we can accept your changes, we do need a signed Puppet + Labs Contributor License Agreement (CLA). + + You can access the CLA via the [Contributor License Agreement link](https://cla.puppetlabs.com/) + + If you have any questions about the CLA, please feel free to + contact Puppet Labs via email at cla-submissions@puppetlabs.com. + + 3. Sending your patches + + To submit your changes via a GitHub pull request, we _highly_ + recommend that you have them on a topic branch, instead of + directly on "master". + It makes things much easier to keep track of, especially if + you decide to work on another thing before your first change + is merged in. + + GitHub has some pretty good + [general documentation](http://help.github.com/) on using + their site. They also have documentation on + [creating pull requests](http://help.github.com/send-pull-requests/). + + In general, after pushing your topic branch up to your + repository on GitHub, you can switch to the branch in the + GitHub UI and click "Pull Request" towards the top of the page + in order to open a pull request. + + + 4. Update the related GitHub issue. + + If there is a GitHub issue associated with the change you + submitted, then you should update the ticket to include the + location of your branch, along with any other commentary you + may wish to make. + +Testing +======= + +Getting Started +--------------- + +Our puppet modules provide [`Gemfile`](./Gemfile)s which can tell a ruby +package manager such as [bundler](http://bundler.io/) what Ruby packages, +or Gems, are required to build, develop, and test this software. + +Please make sure you have [bundler installed](http://bundler.io/#getting-started) +on your system, then use it to install all dependencies needed for this project, +by running + +```shell +% bundle install +Fetching gem metadata from https://rubygems.org/........ +Fetching gem metadata from https://rubygems.org/.. +Using rake (10.1.0) +Using builder (3.2.2) +-- 8><-- many more --><8 -- +Using rspec-system-puppet (2.2.0) +Using serverspec (0.6.3) +Using rspec-system-serverspec (1.0.0) +Using bundler (1.3.5) +Your bundle is complete! +Use `bundle show [gemname]` to see where a bundled gem is installed. +``` + +NOTE some systems may require you to run this command with sudo. + +If you already have those gems installed, make sure they are up-to-date: + +```shell +% bundle update +``` + +With all dependencies in place and up-to-date we can now run the tests: + +```shell +% rake spec +``` + +This will execute all the [rspec tests](http://rspec-puppet.com/) tests +under [spec/defines](./spec/defines), [spec/classes](./spec/classes), +and so on. rspec tests may have the same kind of dependencies as the +module they are testing. While the module defines in its [Modulefile](./Modulefile), +rspec tests define them in [.fixtures.yml](./fixtures.yml). + +Some puppet modules also come with [beaker](https://github.com/puppetlabs/beaker) +tests. These tests spin up a virtual machine under +[VirtualBox](https://www.virtualbox.org/)) with, controlling it with +[Vagrant](http://www.vagrantup.com/) to actually simulate scripted test +scenarios. In order to run these, you will need both of those tools +installed on your system. + +You can run them by issuing the following command + +```shell +% rake spec_clean +% rspec spec/acceptance +``` + +This will now download a pre-fabricated image configured in the [default node-set](./spec/acceptance/nodesets/default.yml), +install puppet, copy this module and install its dependencies per [spec/spec_helper_acceptance.rb](./spec/spec_helper_acceptance.rb) +and then run all the tests under [spec/acceptance](./spec/acceptance). + +Writing Tests +------------- + +XXX getting started writing tests. + +If you have commit access to the repository +=========================================== + +Even if you have commit access to the repository, you will still need to +go through the process above, and have someone else review and merge +in your changes. The rule is that all changes must be reviewed by a +developer on the project (that did not write the code) to ensure that +all changes go through a code review process. + +Having someone other than the author of the topic branch recorded as +performing the merge is the record that they performed the code +review. + + +Additional Resources +==================== + +* [Getting additional help](http://projects.puppetlabs.com/projects/puppet/wiki/Getting_Help) + +* [Writing tests](http://projects.puppetlabs.com/projects/puppet/wiki/Development_Writing_Tests) + +* [Patchwork](https://patchwork.puppetlabs.com) + +* [Contributor License Agreement](https://projects.puppetlabs.com/contributor_licenses/sign) + +* [General GitHub documentation](http://help.github.com/) + +* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) + diff --git a/apache/Gemfile b/apache/Gemfile new file mode 100644 index 000000000..dd87fe8cf --- /dev/null +++ b/apache/Gemfile @@ -0,0 +1,25 @@ +source ENV['GEM_SOURCE'] || "https://rubygems.org" + +group :development, :test do + gem 'rake', :require => false + gem 'rspec-puppet', :require => false + gem 'puppetlabs_spec_helper', :require => false + gem 'serverspec', :require => false + gem 'puppet-lint', :require => false + gem 'beaker', :require => false + gem 'beaker-rspec', :require => false +end + +if facterversion = ENV['FACTER_GEM_VERSION'] + gem 'facter', facterversion, :require => false +else + gem 'facter', :require => false +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/apache/LICENSE b/apache/LICENSE new file mode 100644 index 000000000..8961ce8a6 --- /dev/null +++ b/apache/LICENSE @@ -0,0 +1,15 @@ +Copyright (C) 2012 Puppet Labs Inc + +Puppet Labs can be contacted at: info@puppetlabs.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/apache/Modulefile b/apache/Modulefile new file mode 100644 index 000000000..1a1db1427 --- /dev/null +++ b/apache/Modulefile @@ -0,0 +1,12 @@ +name 'puppetlabs-apache' +version '0.11.0' +source 'git://github.com/puppetlabs/puppetlabs-apache.git' +author 'puppetlabs' +license 'Apache 2.0' +summary 'Puppet module for Apache' +description 'Module for Apache configuration' +project_page 'https://github.com/puppetlabs/puppetlabs-apache' + +## Add dependencies, if any: +dependency 'puppetlabs/stdlib', '>= 2.4.0' +dependency 'puppetlabs/concat', '>= 1.0.0' diff --git a/apache/README.md b/apache/README.md new file mode 100644 index 000000000..fd21507cd --- /dev/null +++ b/apache/README.md @@ -0,0 +1,1745 @@ +#apache + +[![Build Status](https://travis-ci.org/puppetlabs/puppetlabs-apache.png?branch=master)](https://travis-ci.org/puppetlabs/puppetlabs-apache) + +####Table of Contents + +1. [Overview - What is the apache module?](#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with apache](#setup) + * [Beginning with apache - Installation](#beginning-with-apache) + * [Configure a virtual host - Basic options for getting started](#configure-a-virtual-host) +4. [Usage - The classes and defined types available for configuration](#usage) + * [Classes and Defined Types](#classes-and-defined-types) + * [Class: apache](#class-apache) + * [Class: apache::default_mods](#class-apachedefault_mods) + * [Defined Type: apache::mod](#defined-type-apachemod) + * [Classes: apache::mod::*](#classes-apachemodname) + * [Class: apache::mod::ssl](#class-apachemodssl) + * [Class: apache::mod::wsgi](#class-apachemodwsgi) + * [Defined Type: apache::vhost](#defined-type-apachevhost) + * [Parameter: `directories` for apache::vhost](#parameter-directories-for-apachevhost) + * [SSL parameters for apache::vhost](#ssl-parameters-for-apachevhost) + * [Virtual Host Examples - Demonstrations of some configuration options](#virtual-host-examples) + * [Load Balancing](#load-balancing) + * [Defined Type: apache::balancer](#defined-type-apachebalancer) + * [Defined Type: apache::balancermember](#defined-type-apachebalancermember) + * [Examples - Load balancing with exported and non-exported resources](#examples) +5. [Reference - An under-the-hood peek at what the module is doing and how](#reference) + * [Classes](#classes) + * [Public Classes](#public-classes) + * [Private Classes](#private-classes) + * [Defined Types](#defined-types) + * [Public Defined Types](#public-defined-types) + * [Private Defined Types](#private-defined-types) + * [Templates](#templates) +6. [Limitations - OS compatibility, etc.](#limitations) +7. [Development - Guide for contributing to the module](#development) + * [Contributing to the apache module](#contributing) + * [Running tests - A quick guide](#running-tests) + +##Overview + +The apache module allows you to set up virtual hosts and manage web services with minimal effort. + +##Module Description + +Apache is a widely-used web server, and this module provides a simplified way of creating configurations to manage your infrastructure. This includes the ability to configure and manage a range of different virtual host setups, as well as a streamlined way to install and configure Apache modules. + +##Setup + +**What apache affects:** + +* configuration files and directories (created and written to) + * **WARNING**: Configurations that are *not* managed by Puppet will be purged. +* package/service/configuration files for Apache +* Apache modules +* virtual hosts +* listened-to ports +* `/etc/make.conf` on FreeBSD + +###Beginning with Apache + +To install Apache with the default parameters + +```puppet + class { 'apache': } +``` + +The defaults are determined by your operating system (e.g. Debian systems have one set of defaults, and RedHat systems have another, as do FreeBSD systems). These defaults will work well in a testing environment, but are not suggested for production. To establish customized parameters + +```puppet + class { 'apache': + default_mods => false, + default_confd_files => false, + } +``` + +###Configure a virtual host + +Declaring the `apache` class will create a default virtual host by setting up a vhost on port 80, listening on all interfaces and serving `$apache::docroot`. + +```puppet + class { 'apache': } +``` + +To configure a very basic, name-based virtual host + +```puppet + apache::vhost { 'first.example.com': + port => '80', + docroot => '/var/www/first', + } +``` + +*Note:* The default priority is 15. If nothing matches this priority, the alphabetically first name-based vhost will be used. This is also true if you pass a higher priority and no names match anything else. + +A slightly more complicated example, changes the docroot owner/group from the default 'root' + +```puppet + apache::vhost { 'second.example.com': + port => '80', + docroot => '/var/www/second', + docroot_owner => 'third', + docroot_group => 'third', + } +``` + +To set up a virtual host with SSL and default SSL certificates + +```puppet + apache::vhost { 'ssl.example.com': + port => '443', + docroot => '/var/www/ssl', + ssl => true, + } +``` + +To set up a virtual host with SSL and specific SSL certificates + +```puppet + apache::vhost { 'fourth.example.com': + port => '443', + docroot => '/var/www/fourth', + ssl => true, + ssl_cert => '/etc/ssl/fourth.example.com.cert', + ssl_key => '/etc/ssl/fourth.example.com.key', + } +``` + +Virtual hosts listen on '*' by default. To listen on a specific IP address + +```puppet + apache::vhost { 'subdomain.example.com': + ip => '127.0.0.1', + port => '80', + docroot => '/var/www/subdomain', + } +``` + +To set up a virtual host with a wildcard alias for the subdomain mapped to a same-named directory, for example: `http://example.com.loc` to `/var/www/example.com` + +```puppet + apache::vhost { 'subdomain.loc': + vhost_name => '*', + port => '80', + virtual_docroot' => '/var/www/%-2+', + docroot => '/var/www', + serveraliases => ['*.loc',], + } +``` + +To set up a virtual host with suPHP + +```puppet + apache::vhost { 'suphp.example.com': + port => '80', + docroot => '/home/appuser/myphpapp', + suphp_addhandler => 'x-httpd-php', + suphp_engine => 'on', + suphp_configpath => '/etc/php5/apache2', + directories => { path => '/home/appuser/myphpapp', + 'suphp' => { user => 'myappuser', group => 'myappgroup' }, + } + } +``` + +To set up a virtual host with WSGI + +```puppet + apache::vhost { 'wsgi.example.com': + port => '80', + docroot => '/var/www/pythonapp', + wsgi_application_group => '%{GLOBAL}', + wsgi_daemon_process => 'wsgi', + wsgi_daemon_process_options => { + processes => '2', + threads => '15', + display-name => '%{GROUP}', + }, + wsgi_import_script => '/var/www/demo.wsgi', + wsgi_import_script_options => + { process-group => 'wsgi', application-group => '%{GLOBAL}' }, + wsgi_process_group => 'wsgi', + wsgi_script_aliases => { '/' => '/var/www/demo.wsgi' }, + } +``` + +Starting in Apache 2.2.16, HTTPD supports [FallbackResource](https://httpd.apache.org/docs/current/mod/mod_dir.html#fallbackresource), a simple replacement for common RewriteRules. + +```puppet + apache::vhost { 'wordpress.example.com': + port => '80', + docroot => '/var/www/wordpress', + fallbackresource => '/index.php', + } +``` + +Please note that the 'disabled' argument to FallbackResource is only supported since Apache 2.2.24. + +See a list of all [virtual host parameters](#defined-type-apachevhost). See an extensive list of [virtual host examples](#virtual-host-examples). + +##Usage + +###Classes and Defined Types + +This module modifies Apache configuration files and directories, and will purge any configuration not managed by Puppet. Configuration of Apache should be managed by Puppet, as non-Puppet configuration files can cause unexpected failures. + +It is possible to temporarily disable full Puppet management by setting the [`purge_configs`](#purge_configs) parameter within the base `apache` class to 'false'. This option should only be used as a temporary means of saving and relocating customized configurations. See the [`purge_configs` parameter](#purge_configs) for more information. + +####Class: `apache` + +The apache module's primary class, `apache`, guides the basic setup of Apache on your system. + +You may establish a default vhost in this class, the `vhost` class, or both. You may add additional vhost configurations for specific virtual hosts using a declaration of the `vhost` type. + +**Parameters within `apache`:** + +#####`apache_version` + +Configures the behavior of the module templates, package names, and default mods by setting the Apache version. Default is determined by the class `apache::version` using the OS family and release. It should not be configured manually without special reason. + +#####`confd_dir` + +Changes the location of the configuration directory your custom configuration files are placed in. Defaults to '/etc/httpd/conf' on RedHat, '/etc/apache2' on Debian, and '/usr/local/etc/apache22' on FreeBSD. + +#####`conf_template` + +Overrides the template used for the main apache configuration file. Defaults to 'apache/httpd.conf.erb'. + +*Note:* Using this parameter is potentially risky, as the module has been built for a minimal configuration file with the configuration primarily coming from conf.d/ entries. + +#####`default_confd_files` + +Generates default set of include-able Apache configuration files under `${apache::confd_dir}` directory. These configuration files correspond to what is usually installed with the Apache package on a given platform. + +#####`default_mods` + +Sets up Apache with default settings based on your OS. Valid values are 'true', 'false', or an array of mod names. + +Defaults to 'true', which will include the default [HTTPD mods](https://github.com/puppetlabs/puppetlabs-apache/blob/master/manifests/default_mods.pp). + +If false, it will only include the mods required to make HTTPD work, and any other mods can be declared on their own. + +If an array, the apache module will include the array of mods listed. + +#####`default_ssl_ca` + +The default certificate authority, which is automatically set to 'undef'. This default will work out of the box but must be updated with your specific certificate information before being used in production. + +#####`default_ssl_cert` + +The default SSL certification, which is automatically set based on your operating system ('/etc/pki/tls/certs/localhost.crt' for RedHat, '/etc/ssl/certs/ssl-cert-snakeoil.pem' for Debian, and '/usr/local/etc/apache22/server.crt' for FreeBSD). This default will work out of the box but must be updated with your specific certificate information before being used in production. + +#####`default_ssl_chain` + +The default SSL chain, which is automatically set to 'undef'. This default will work out of the box but must be updated with your specific certificate information before being used in production. + +#####`default_ssl_crl` + +The default certificate revocation list to use, which is automatically set to 'undef'. This default will work out of the box but must be updated with your specific certificate information before being used in production. + +#####`default_ssl_crl_path` + +The default certificate revocation list path, which is automatically set to 'undef'. This default will work out of the box but must be updated with your specific certificate information before being used in production. + +#####`default_ssl_key` + +The default SSL key, which is automatically set based on your operating system ('/etc/pki/tls/private/localhost.key' for RedHat, '/etc/ssl/private/ssl-cert-snakeoil.key' for Debian, and '/usr/local/etc/apache22/server.key' for FreeBSD). This default will work out of the box but must be updated with your specific certificate information before being used in production. + +#####`default_ssl_vhost` + +Sets up a default SSL virtual host. Defaults to 'false'. If set to 'true', will set up the following vhost: + +```puppet + apache::vhost { 'default-ssl': + port => 443, + ssl => true, + docroot => $docroot, + scriptalias => $scriptalias, + serveradmin => $serveradmin, + access_log_file => "ssl_${access_log_file}", + } +``` + +SSL vhosts only respond to HTTPS queries. + +#####`default_vhost` + +Sets up a default virtual host. Defaults to 'true', set to 'false' to set up [customized virtual hosts](#configure-a-virtual-host). + +#####`error_documents` + +Enables custom error documents. Defaults to 'false'. + +#####`httpd_dir` + +Changes the base location of the configuration directories used for the apache service. This is useful for specially repackaged HTTPD builds, but may have unintended consequences when used in combination with the default distribution packages. Defaults to '/etc/httpd' on RedHat, '/etc/apache2' on Debian, and '/usr/local/etc/apache22' on FreeBSD. + +#####`keepalive` + +Enables persistent connections. + +#####`keepalive_timeout` + +Sets the amount of time the server will wait for subsequent requests on a persistent connection. Defaults to '15'. + +#####`log_level` + +Changes the verbosity level of the error log. Defaults to 'warn'. Valid values are 'emerg', 'alert', 'crit', 'error', 'warn', 'notice', 'info', or 'debug'. + +#####`logroot` + +Changes the directory where Apache log files for the virtual host are placed. Defaults to '/var/log/httpd' on RedHat, '/var/log/apache2' on Debian, and '/var/log/apache22' on FreeBSD. + +#####`manage_group` + +Setting this to 'false' will stop the group resource from being created. This is for when you have a group, created from another Puppet module, you want to use to run Apache. Without this parameter, attempting to use a previously established group would result in a duplicate resource error. + +#####`manage_user` + +Setting this to 'false' will stop the user resource from being created. This is for instances when you have a user, created from another Puppet module, you want to use to run Apache. Without this parameter, attempting to use a previously established user would result in a duplicate resource error. + +#####`mod_dir` + +Changes the location of the configuration directory your Apache modules configuration files are placed in. Defaults to '/etc/httpd/conf.d' for RedHat, '/etc/apache2/mods-available' for Debian, and '/usr/local/etc/apache22/Modules' for FreeBSD. + +#####`mpm_module` + +Determines which MPM is loaded and configured for the HTTPD process. Valid values are 'event', 'itk', 'peruser', 'prefork', 'worker', or 'false'. Defaults to 'prefork' on RedHat and FreeBSD, and 'worker' on Debian. Must be set to 'false' to explicitly declare the following classes with custom parameters: + +* `apache::mod::event` +* `apache::mod::itk` +* `apache::mod::peruser` +* `apache::mod::prefork` +* `apache::mod::worker` + +*Note:* Switching between different MPMs on FreeBSD is possible but quite difficult. Before changing `$mpm_module` you must uninstall all packages that depend on your currently-installed Apache. + +#####`package_ensure` + +Allows control over the package ensure attribute. Can be 'present','absent', or a version string. + +#####`ports_file` + +Changes the name of the file containing Apache ports configuration. Default is `${conf_dir}/ports.conf`. + +#####`purge_configs` + +Removes all other Apache configs and vhosts, defaults to 'true'. Setting this to 'false' is a stopgap measure to allow the apache module to coexist with existing or otherwise-managed configuration. It is recommended that you move your configuration entirely to resources within this module. + +#####`sendfile` + +Makes Apache use the Linux kernel sendfile to serve static files. Defaults to 'On'. + +#####`serveradmin` + +Sets the server administrator. Defaults to 'root@localhost'. + +#####`servername` + +Sets the server name. Defaults to `fqdn` provided by Facter. + +#####`server_root` + +Sets the root directory in which the server resides. Defaults to '/etc/httpd' on RedHat, '/etc/apache2' on Debian, and '/usr/local' on FreeBSD. + +#####`server_signature` + +Configures a trailing footer line under server-generated documents. More information about [ServerSignature](http://httpd.apache.org/docs/current/mod/core.html#serversignature). Defaults to 'On'. + +#####`server_tokens` + +Controls how much information Apache sends to the browser about itself and the operating system. More information about [ServerTokens](http://httpd.apache.org/docs/current/mod/core.html#servertokens). Defaults to 'OS'. + +#####`service_enable` + +Determines whether the HTTPD service is enabled when the machine is booted. Defaults to 'true'. + +#####`service_ensure` + +Determines whether the service should be running. Can be set to 'undef', which is useful when you want to let the service be managed by some other application like Pacemaker. Defaults to 'running'. + +#####`service_name` + +Name of the Apache service to run. Defaults to: 'httpd' on RedHat, 'apache2' on Debian, and 'apache22' on FreeBSD. + +#####`trace_enable` + +Controls how TRACE requests per RFC 2616 are handled. More information about [TraceEnable](http://httpd.apache.org/docs/current/mod/core.html#traceenable). Defaults to 'On'. + +#####`vhost_dir` + +Changes the location of the configuration directory your virtual host configuration files are placed in. Defaults to 'etc/httpd/conf.d' on RedHat, '/etc/apache2/sites-available' on Debian, and '/usr/local/etc/apache22/Vhosts' on FreeBSD. + +####Class: `apache::default_mods` + +Installs default Apache modules based on what OS you are running. + +```puppet + class { 'apache::default_mods': } +``` + +####Defined Type: `apache::mod` + +Used to enable arbitrary Apache HTTPD modules for which there is no specific `apache::mod::[name]` class. The `apache::mod` defined type will also install the required packages to enable the module, if any. + +```puppet + apache::mod { 'rewrite': } + apache::mod { 'ldap': } +``` + +####Classes: `apache::mod::[name]` + +There are many `apache::mod::[name]` classes within this module that can be declared using `include`: + +* `alias` +* `auth_basic` +* `auth_kerb` +* `authnz_ldap`* +* `autoindex` +* `cache` +* `cgi` +* `cgid` +* `dav` +* `dav_fs` +* `dav_svn` +* `deflate` +* `dev` +* `dir`* +* `disk_cache` +* `event` +* `expires` +* `fastcgi` +* `fcgid` +* `headers` +* `include` +* `info` +* `itk` +* `ldap` +* `mime` +* `mime_magic`* +* `negotiation` +* `nss`* +* `passenger`* +* `perl` +* `peruser` +* `php` (requires [`mpm_module`](#mpm_module) set to `prefork`) +* `prefork`* +* `proxy`* +* `proxy_ajp` +* `proxy_balancer` +* `proxy_html` +* `proxy_http` +* `python` +* `reqtimeout` +* `rewrite` +* `rpaf`* +* `setenvif` +* `ssl`* (see [`apache::mod::ssl`](#class-apachemodssl) below) +* `status`* +* `suphp` +* `userdir`* +* `vhost_alias` +* `worker`* +* `wsgi` (see [`apache::mod::wsgi`](#class-apachemodwsgi) below) +* `xsendfile` + +Modules noted with a * indicate that the module has settings and, thus, a template that includes parameters. These parameters control the module's configuration. Most of the time, these parameters will not require any configuration or attention. + +The modules mentioned above, and other Apache modules that have templates, will cause template files to be dropped along with the mod install and the module will not work without the template. Any module without a template will install the package but drop no files. + +####Class: `apache::mod::ssl` + +Installs Apache SSL capabilities and uses the ssl.conf.erb template. These are the defaults: + +```puppet + class { 'apache::mod::ssl': + ssl_compression => false, + ssl_options => [ 'StdEnvVars' ], + } +``` + +To *use* SSL with a virtual host, you must either set the`default_ssl_vhost` parameter in `::apache` to 'true' or set the `ssl` parameter in `apache::vhost` to 'true'. + +####Class: `apache::mod::wsgi` + +Enables Python support in the WSGI module. To use, simply `include 'apache::mod::wsgi'`. + +For customized parameters, which tell Apache how Python is currently configured on the operating system, + +```puppet + class { 'apache::mod::wsgi': + wsgi_socket_prefix => "\${APACHE_RUN_DIR}WSGI", + wsgi_python_home => '/path/to/venv', + wsgi_python_path => '/path/to/venv/site-packages', + } +``` + +More information about [WSGI](http://modwsgi.readthedocs.org/en/latest/). + +####Defined Type: `apache::vhost` + +The Apache module allows a lot of flexibility in the setup and configuration of virtual hosts. This flexibility is due, in part, to `vhost`'s being a defined resource type, which allows it to be evaluated multiple times with different parameters. + +The `vhost` defined type allows you to have specialized configurations for virtual hosts that have requirements outside the defaults. You can set up a default vhost within the base `::apache` class, as well as set a customized vhost as default. Your customized vhost (priority 10) will be privileged over the base class vhost (15). + +If you have a series of specific configurations and do not want a base `::apache` class default vhost, make sure to set the base class `default_vhost` to 'false'. + +```puppet + class { 'apache': + default_vhost => false, + } +``` + +**Parameters within `apache::vhost`:** + +#####`access_log` + +Specifies whether `*_access.log` directives (`*_file`,`*_pipe`, or `*_syslog`) should be configured. Setting the value to 'false' will choose none. Defaults to 'true'. + +#####`access_log_file` + +Sets the `*_access.log` filename that is placed in `$logroot`. Given a vhost, example.com, it defaults to 'example.com_ssl.log' for SSL vhosts and 'example.com_access.log' for non-SSL vhosts. + +#####`access_log_pipe` + +Specifies a pipe to send access log messages to. Defaults to 'undef'. + +#####`access_log_syslog` + +Sends all access log messages to syslog. Defaults to 'undef'. + +#####`access_log_format` + +Specifies the use of either a LogFormat nickname or a custom format string for the access log. Defaults to 'combined'. See [these examples](http://httpd.apache.org/docs/current/mod/mod_log_config.html). + +#####`access_log_env_var` + +Specifies that only requests with particular environment variables be logged. Defaults to 'undef'. + +#####`add_listen` + +Determines whether the vhost creates a Listen statement. The default value is 'true'. + +Setting `add_listen` to 'false' stops the vhost from creating a Listen statement, and this is important when you combine vhosts that are not passed an `ip` parameter with vhosts that *are* passed the `ip` parameter. + +#####`additional_includes` + +Specifies paths to additional static, vhost-specific Apache configuration files. Useful for implementing a unique, custom configuration not supported by this module. Can be an array. Defaults to '[]'. + +#####`aliases` + +Passes a list of hashes to the vhost to create Alias or AliasMatch directives as per the [mod_alias documentation](http://httpd.apache.org/docs/current/mod/mod_alias.html). These hashes are formatted as follows: + +```puppet +aliases => [ + { aliasmatch => '^/image/(.*)\.jpg$', + path => '/files/jpg.images/$1.jpg', + } + { alias => '/image', + path => '/ftp/pub/image', + }, +], +``` + +For `alias` and `aliasmatch` to work, each will need a corresponding context, such as '< Directory /path/to/directory>' or ''. The Alias and AliasMatch directives are created in the order specified in the `aliases` parameter. As described in the [`mod_alias` documentation](http://httpd.apache.org/docs/current/mod/mod_alias.html), more specific `alias` or `aliasmatch` parameters should come before the more general ones to avoid shadowing. + +*Note:* If `apache::mod::passenger` is loaded and `PassengerHighPerformance => true` is set, then Alias may have issues honoring the `PassengerEnabled => off` statement. See [this article](http://www.conandalton.net/2010/06/passengerenabled-off-not-working.html) for details. + +#####`block` + +Specifies the list of things Apache will block access to. The default is an empty set, '[]'. Currently, the only option is 'scm', which blocks web access to .svn, .git and .bzr directories. + +#####`custom_fragment` + +Passes a string of custom configuration directives to be placed at the end of the vhost configuration. Defaults to 'undef'. + +#####`default_vhost` + +Sets a given `apache::vhost` as the default to serve requests that do not match any other `apache::vhost` definitions. The default value is 'false'. + +#####`directories` + +See the [`directories` section](#parameter-directories-for-apachevhost). + +#####`directoryindex` + +Sets the list of resources to look for when a client requests an index of the directory by specifying a '/' at the end of the directory name. [DirectoryIndex](http://httpd.apache.org/docs/current/mod/mod_dir.html#directoryindex) has more information. Defaults to 'undef'. + +#####`docroot` + +Provides the [DocumentRoot](http://httpd.apache.org/docs/current/mod/core.html#documentroot) directive, which identifies the directory Apache serves files from. Required. + +#####`docroot_group` + +Sets group access to the docroot directory. Defaults to 'root'. + +#####`docroot_owner` + +Sets individual user access to the docroot directory. Defaults to 'root'. + +#####`error_log` + +Specifies whether `*_error.log` directives should be configured. Defaults to 'true'. + +#####`error_log_file` + +Points to the `*_error.log` file. Given a vhost, example.com, it defaults to 'example.com_ssl_error.log' for SSL vhosts and 'example.com_access_error.log' for non-SSL vhosts. + +#####`error_log_pipe` + +Specifies a pipe to send error log messages to. Defaults to 'undef'. + +#####`error_log_syslog` + +Sends all error log messages to syslog. Defaults to 'undef'. + +#####`error_documents` + +A list of hashes which can be used to override the [ErrorDocument](https://httpd.apache.org/docs/current/mod/core.html#errordocument) settings for this vhost. Defaults to '[]'. Example: + +```puppet + apache::vhost { 'sample.example.net': + error_documents => [ + { 'error_code' => '503', 'document' => '/service-unavail' }, + { 'error_code' => '407', 'document' => 'https://example.com/proxy/login' }, + ], + } +``` + +#####`ensure` + +Specifies if the vhost file is present or absent. Defaults to 'present'. + +#####`fallbackresource` + +Sets the [FallbackResource](http://httpd.apache.org/docs/current/mod/mod_dir.html#fallbackresource) directive, which specifies an action to take for any URL that doesn't map to anything in your filesystem and would otherwise return 'HTTP 404 (Not Found)'. Valid values must either begin with a / or be 'disabled'. Defaults to 'undef'. + +#####`headers` + +Adds lines to replace, merge, or remove response headers. See [Header](http://httpd.apache.org/docs/current/mod/mod_headers.html#header) for more information. Can be an array. Defaults to 'undef'. + +#####`ip` + +Sets the IP address the vhost listens on. Defaults to listen on all IPs. + +#####`ip_based` + +Enables an [IP-based](httpd.apache.org/docs/current/vhosts/ip-based.html) vhost. This parameter inhibits the creation of a NameVirtualHost directive, since those are used to funnel requests to name-based vhosts. Defaults to 'false'. + +#####`itk` + +Configures [ITK](http://mpm-itk.sesse.net/) in a hash. Keys may be: + +* user + group +* `assignuseridexpr` +* `assigngroupidexpr` +* `maxclientvhost` +* `nice` +* `limituidrange` (Linux 3.5.0 or newer) +* `limitgidrange` (Linux 3.5.0 or newer) + +Usage will typically look like: + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + itk => { + user => 'someuser', + group => 'somegroup', + }, + } +``` + +#####`logroot` + +Specifies the location of the virtual host's logfiles. Defaults to '/var/log//'. + +#####`log_level` + +Specifies the verbosity of the error log. Defaults to 'warn' for the global server configuration and can be overridden on a per-vhost basis. Valid values are 'emerg', 'alert', 'crit', 'error', 'warn', 'notice', 'info' or 'debug'. + +#####`no_proxy_uris` + +Specifies URLs you do not want to proxy. This parameter is meant to be used in combination with [`proxy_dest`](#proxy_dest). + +#####`options` + +Sets the [Options](http://httpd.apache.org/docs/current/mod/core.html#options) for the specified virtual host. Defaults to '['Indexes','FollowSymLinks','MultiViews']', as demonstrated below: + +```puppet + apache::vhost { 'site.name.fdqn': + … + options => ['Indexes','FollowSymLinks','MultiViews'], + } +``` + +*Note:* If you use [`directories`](#parameter-directories-for-apachevhost), 'Options', 'Override', and 'DirectoryIndex' are ignored because they are parameters within `directories`. + +#####`override` + +Sets the overrides for the specified virtual host. Accepts an array of [AllowOverride](http://httpd.apache.org/docs/current/mod/core.html#allowoverride) arguments. Defaults to '[none]'. + +#####`php_admin_flags & values` + +Allows per-vhost setting [`php_admin_value`s or `php_admin_flag`s](http://php.net/manual/en/configuration.changes.php). These flags or values cannot be overwritten by a user or an application. Defaults to '[]'. + +#####`port` + +Sets the port the host is configured on. The module's defaults ensure the host listens on port 80 for non-SSL vhosts and port 443 for SSL vhosts. The host will only listen on the port set in this parameter. + +#####`priority` + +Sets the relative load-order for Apache HTTPD VirtualHost configuration files. Defaults to '25'. + +If nothing matches the priority, the first name-based vhost will be used. Likewise, passing a higher priority will cause the alphabetically first name-based vhost to be used if no other names match. + +*Note:* You should not need to use this parameter. However, if you do use it, be aware that the `default_vhost` parameter for `apache::vhost` passes a priority of '15'. + +#####`proxy_dest` + +Specifies the destination address of a [ProxyPass](http://httpd.apache.org/docs/current/mod/mod_proxy.html#proxypass) configuration. Defaults to 'undef'. + +#####`proxy_pass` + +Specifies an array of `path => URI` for a [ProxyPass](http://httpd.apache.org/docs/current/mod/mod_proxy.html#proxypass) configuration. Defaults to 'undef'. + +```puppet +apache::vhost { 'site.name.fdqn': + … + proxy_pass => [ + { 'path' => '/a', 'url' => 'http://backend-a/' }, + { 'path' => '/b', 'url' => 'http://backend-b/' }, + { 'path' => '/c', 'url' => 'http://backend-a/c' }, + ], +} +``` + +#####`rack_base_uris` + +Specifies the resource identifiers for a rack configuration. The file paths specified will be listed as rack application roots for [Phusion Passenger](http://www.modrails.com/documentation/Users%20guide%20Apache.html#_railsbaseuri_and_rackbaseuri) in the _rack.erb template. Defaults to 'undef'. + +#####`redirect_dest` + +Specifies the address to redirect to. Defaults to 'undef'. + +#####`redirect_source` + +Specifies the source URIs that will redirect to the destination specified in `redirect_dest`. If more than one item for redirect is supplied, the source and destination must be the same length and the items will be order-dependent. + +```puppet + apache::vhost { 'site.name.fdqn': + … + redirect_source => ['/images','/downloads'], + redirect_dest => ['http://img.example.com/','http://downloads.example.com/'], + } +``` + +#####`redirect_status` + +Specifies the status to append to the redirect. Defaults to 'undef'. + +```puppet + apache::vhost { 'site.name.fdqn': + … + redirect_status => ['temp','permanent'], + } +``` + +#####`redirectmatch_regexp` & `redirectmatch_status` + +Determines which server status should be raised for a given regular expression. Entered as an array. Defaults to 'undef'. + +```puppet + apache::vhost { 'site.name.fdqn': + … + redirectmatch_status => ['404','404'], + redirectmatch_regexp => ['\.git(/.*|$)/','\.svn(/.*|$)'], + } +``` + +#####`request_headers` + +Modifies collected [request headers](http://httpd.apache.org/docs/current/mod/mod_headers.html#requestheader) in various ways, including adding additional request headers, removing request headers, etc. Defaults to 'undef'. + +```puppet + apache::vhost { 'site.name.fdqn': + … + request_headers => [ + 'append MirrorID "mirror 12"', + 'unset MirrorID', + ], + } +``` + +#####`rewrites` + +Creates URL rewrite rules. Expects an array of hashes, and the hash keys can be any of 'comment', 'rewrite_base', 'rewrite_cond', or 'rewrite_rule'. Defaults to 'undef'. + +For example, you can specify that anyone trying to access index.html will be served welcome.html + +```puppet + apache::vhost { 'site.name.fdqn': + … + rewrites => [ { rewrite_rule => ['^index\.html$ welcome.html'] } ] + } +``` + +The parameter allows rewrite conditions that, when true, will execute the associated rule. For instance, if you wanted to rewrite URLs only if the visitor is using IE + +```puppet + apache::vhost { 'site.name.fdqn': + … + rewrites => [ + { + comment => 'redirect IE', + rewrite_cond => ['%{HTTP_USER_AGENT} ^MSIE'], + rewrite_rule => ['^index\.html$ welcome.html'], + }, + ], + } +``` + +You can also apply multiple conditions. For instance, rewrite index.html to welcome.html only when the browser is Lynx or Mozilla (version 1 or 2) + +```puppet + apache::vhost { 'site.name.fdqn': + … + rewrites => [ + { + comment => 'Lynx or Mozilla v1/2', + rewrite_cond => ['%{HTTP_USER_AGENT} ^Lynx/ [OR]', '%{HTTP_USER_AGENT} ^Mozilla/[12]'], + rewrite_rule => ['^index\.html$ welcome.html'], + }, + ], + } +``` + +Multiple rewrites and conditions are also possible + +```puppet + apache::vhost { 'site.name.fdqn': + … + rewrites => [ + { + comment => 'Lynx or Mozilla v1/2', + rewrite_cond => ['%{HTTP_USER_AGENT} ^Lynx/ [OR]', '%{HTTP_USER_AGENT} ^Mozilla/[12]'], + rewrite_rule => ['^index\.html$ welcome.html'], + }, + { + comment => 'Internet Explorer', + rewrite_cond => ['%{HTTP_USER_AGENT} ^MSIE'], + rewrite_rule => ['^index\.html$ /index.IE.html [L]'], + }, + } + rewrite_base => /apps/, + rewrite_rule => ['^index\.cgi$ index.php', '^index\.html$ index.php', '^index\.asp$ index.html'], + }, + ], + } +``` + +Refer to the [`mod_rewrite` documentation](http://httpd.apache.org/docs/current/mod/mod_rewrite.html) for more details on what is possible with rewrite rules and conditions. + +#####`scriptalias` + +Defines a directory of CGI scripts to be aliased to the path '/cgi-bin', for example: '/usr/scripts'. Defaults to 'undef'. + +#####`scriptaliases` + +Passes an array of hashes to the vhost to create either ScriptAlias or ScriptAliasMatch statements as per the [`mod_alias` documentation](http://httpd.apache.org/docs/current/mod/mod_alias.html). These hashes are formatted as follows: + +```puppet + scriptaliases => [ + { + alias => '/myscript', + path => '/usr/share/myscript', + }, + { + aliasmatch => '^/foo(.*)', + path => '/usr/share/fooscripts$1', + }, + { + aliasmatch => '^/bar/(.*)', + path => '/usr/share/bar/wrapper.sh/$1', + }, + { + alias => '/neatscript', + path => '/usr/share/neatscript', + }, + ] +``` + +The ScriptAlias and ScriptAliasMatch directives are created in the order specified. As with [Alias and AliasMatch](#aliases) directives, more specific aliases should come before more general ones to avoid shadowing. + +#####`serveradmin` + +Specifies the email address Apache will display when it renders one of its error pages. Defaults to 'undef'. + +#####`serveraliases` + +Sets the [ServerAliases](http://httpd.apache.org/docs/current/mod/core.html#serveralias) of the site. Defaults to '[]'. + +#####`servername` + +Sets the servername corresponding to the hostname you connect to the virtual host at. Defaults to the title of the resource. + +#####`setenv` + +Used by HTTPD to set environment variables for vhosts. Defaults to '[]'. + +#####`setenvif` + +Used by HTTPD to conditionally set environment variables for vhosts. Defaults to '[]'. + +#####`suphp_addhandler`, `suphp_configpath`, & `suphp_engine` + +Set up a virtual host with [suPHP](http://suphp.org/DocumentationView.html?file=apache/CONFIG). + +`suphp_addhandler` defaults to 'php5-script' on RedHat and FreeBSD, and 'x-httpd-php' on Debian. + +`suphp_configpath` defaults to 'undef' on RedHat and FreeBSD, and '/etc/php5/apache2' on Debian. + +`suphp_engine` allows values 'on' or 'off'. Defaults to 'off' + +To set up a virtual host with suPHP + +```puppet + apache::vhost { 'suphp.example.com': + port => '80', + docroot => '/home/appuser/myphpapp', + suphp_addhandler => 'x-httpd-php', + suphp_engine => 'on', + suphp_configpath => '/etc/php5/apache2', + directories => { path => '/home/appuser/myphpapp', + 'suphp' => { user => 'myappuser', group => 'myappgroup' }, + } + } +``` + +#####`vhost_name` + +Enables name-based virtual hosting. If no IP is passed to the virtual host but the vhost is assigned a port, then the vhost name will be 'vhost_name:port'. If the virtual host has no assigned IP or port, the vhost name will be set to the title of the resource. Defaults to '*'. + +#####`virtual_docroot` + +Sets up a virtual host with a wildcard alias subdomain mapped to a directory with the same name. For example, 'http://example.com' would map to '/var/www/example.com'. Defaults to 'false'. + +```puppet + apache::vhost { 'subdomain.loc': + vhost_name => '*', + port => '80', + virtual_docroot' => '/var/www/%-2+', + docroot => '/var/www', + serveraliases => ['*.loc',], + } +``` + +#####`wsgi_daemon_process`, `wsgi_daemon_process_options`, `wsgi_process_group`, & `wsgi_script_aliases` + +Set up a virtual host with [WSGI](https://code.google.com/p/modwsgi/). + +`wsgi_daemon_process` sets the name of the WSGI daemon. It is a hash, accepting [these keys](http://modwsgi.readthedocs.org/en/latest/configuration-directives/WSGIDaemonProcess.html), and it defaults to 'undef'. + +`wsgi_daemon_process_options` is optional and defaults to 'undef'. + +`wsgi_process_group` sets the group ID the virtual host will run under. Defaults to 'undef'. + +`wsgi_script_aliases` requires a hash of web paths to filesystem .wsgi paths. Defaults to 'undef'. + +To set up a virtual host with WSGI + +```puppet + apache::vhost { 'wsgi.example.com': + port => '80', + docroot => '/var/www/pythonapp', + wsgi_daemon_process => 'wsgi', + wsgi_daemon_process_options => + { processes => '2', + threads => '15', + display-name => '%{GROUP}', + }, + wsgi_process_group => 'wsgi', + wsgi_script_aliases => { '/' => '/var/www/demo.wsgi' }, + } +``` + +####Parameter `directories` for `apache::vhost` + +The `directories` parameter within the `apache::vhost` class passes an array of hashes to the vhost to create [Directory](http://httpd.apache.org/docs/current/mod/core.html#directory), [File](http://httpd.apache.org/docs/current/mod/core.html#files), and [Location](http://httpd.apache.org/docs/current/mod/core.html#location) directive blocks. These blocks take the form, '< Directory /path/to/directory>...< /Directory>'. + +Each hash passed to `directories` must contain `path` as one of the keys. You may also pass in `provider` which, if missing, defaults to 'directory'. (A full list of acceptable keys is below.) General usage will look something like + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', => }, + { path => '/path/to/another/directory', => }, + ], + } +``` + +*Note:* At least one directory should match the `docroot` parameter. Once you start declaring directories, `apache::vhost` assumes that all required Directory blocks will be declared. If not defined, a single default Directory block will be created that matches the `docroot` parameter. + +The `provider` key can be set to 'directory', 'files', or 'location'. If the path starts with a [~](https://httpd.apache.org/docs/current/mod/core.html#files), HTTPD will interpret this as the equivalent of DirectoryMatch, FilesMatch, or LocationMatch. + +```puppet + apache::vhost { 'files.example.net': + docroot => '/var/www/files', + directories => [ + { 'path' => '/var/www/files', + 'provider' => 'files', + 'deny' => 'from all' + }, + ], + } +``` + +Available handlers, represented as keys, should be placed within the `directory`,`'files`, or `location` hashes. This looks like + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ { path => '/path/to/directory', handler => value } ], +} +``` + +Any handlers you do not set in these hashes will be considered 'undefined' within Puppet and will not be added to the virtual host, resulting in the module using their default values. Currently this is the list of supported handlers: + +######`addhandlers` + +Sets [AddHandler](http://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler) directives, which map filename extensions to the specified handler. Accepts a list of hashes, with `extensions` serving to list the extensions being managed by the handler, and takes the form: `{ handler => 'handler-name', extensions => ['extension']}`. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + addhandlers => [{ handler => 'cgi-script', extensions => ['.cgi']}], + }, + ], + } +``` + +######`allow` + +Sets an [Allow](http://httpd.apache.org/docs/2.2/mod/mod_authz_host.html#allow) directive, which groups authorizations based on hostnames or IPs. **Deprecated:** This parameter is being deprecated due to a change in Apache. It will only work with Apache 2.2 and lower. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + allow => 'from example.org', + }, + ], + } +``` + +######`allow_override` + +Sets the types of directives allowed in [.htaccess](http://httpd.apache.org/docs/current/mod/core.html#allowoverride) files. Accepts an array. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + allow_override => ['AuthConfig', 'Indexes'], + }, + ], + } +``` + +######`auth_basic_authoritative` + +Sets the value for [AuthBasicAuthoritative](https://httpd.apache.org/docs/current/mod/mod_auth_basic.html#authbasicauthoritative), which determines whether authorization and authentication are passed to lower level Apache modules. + +######`auth_basic_fake` + +Sets the value for [AuthBasicFake](httpd.apache.org/docs/current/mod/mod_auth_basic.html#authbasicfake), which statically configures authorization credentials for a given directive block. + +######`auth_basic_provider` + +Sets the value for [AuthBasicProvider] (httpd.apache.org/docs/current/mod/mod_auth_basic.html#authbasicprovider), which sets the authentication provider for a given location. + +######`auth_digest_algorithm` + +Sets the value for [AuthDigestAlgorithm](httpd.apache.org/docs/current/mod/mod_auth_digest.html#authdigestalgorithm), which selects the algorithm used to calculate the challenge and response hashes. + +######`auth_digest_domain` + +Sets the value for [AuthDigestDomain](httpd.apache.org/docs/current/mod/mod_auth_digest.html#authdigestdomain), which allows you to specify one or more URIs in the same protection space for digest authentication. + +######`auth_digest_nonce_lifetime` + +Sets the value for [AuthDigestNonceLifetime](httpd.apache.org/docs/current/mod/mod_auth_digest.html#authdigestnoncelifetime), which controls how long the server nonce is valid. + +######`auth_digest_provider` + +Sets the value for [AuthDigestProvider](httpd.apache.org/docs/current/mod/mod_auth_digest.html#authdigestprovider), which sets the authentication provider for a given location. + +######`auth_digest_qop` + +Sets the value for [AuthDigestQop](httpd.apache.org/docs/current/mod/mod_auth_digest.html#authdigestqop), which determines the quality-of-protection to use in digest authentication. + +######`auth_digest_shmem_size` + +Sets the value for [AuthAuthDigestShmemSize](httpd.apache.org/docs/current/mod/mod_auth_digest.html#authdigestshmemsize), which defines the amount of shared memory allocated to the server for keeping track of clients. + +######`auth_group_file` + +Sets the value for [AuthGroupFile](https://httpd.apache.org/docs/current/mod/mod_authz_groupfile.html#authgroupfile), which sets the name of the text file containing the list of user groups for authorization. + +######`auth_name` + +Sets the value for [AuthName](http://httpd.apache.org/docs/current/mod/mod_authn_core.html#authname), which sets the name of the authorization realm. + +######`auth_require` + +Sets the entity name you're requiring to allow access. Read more about [Require](http://httpd.apache.org/docs/current/mod/mod_authz_host.html#requiredirectives). + +######`auth_type` + +Sets the value for [AuthType](httpd.apache.org/docs/current/mod/mod_authn_core.html#authtype), which guides the type of user authentication. + +######`auth_user_file` + +Sets the value for [AuthUserFile](httpd.apache.org/docs/current/mod/mod_authn_file.html#authuserfile), which sets the name of the text file containing the users/passwords for authentication. + +######`custom_fragment` + +Pass a string of custom configuration directives to be placed at the end of the directory configuration. + +```puppet + apache::vhost { 'monitor': + … + custom_fragment => ' + + SetHandler balancer-manager + Order allow,deny + Allow from all + + + SetHandler server-status + Order allow,deny + Allow from all + + ProxyStatus On', +} +``` + +######`deny` + +Sets a [Deny](http://httpd.apache.org/docs/2.2/mod/mod_authz_host.html#deny) directive, specifying which hosts are denied access to the server. **Deprecated:** This parameter is being deprecated due to a change in Apache. It will only work with Apache 2.2 and lower. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + deny => 'from example.org', + }, + ], + } +``` + +######`error_documents` + +An array of hashes used to override the [ErrorDocument](https://httpd.apache.org/docs/current/mod/core.html#errordocument) settings for the directory. + +```puppet + apache::vhost { 'sample.example.net': + directories => [ + { path => '/srv/www', + error_documents => [ + { 'error_code' => '503', + 'document' => '/service-unavail', + }, + ], + }, + ], + } +``` + +######`headers` + +Adds lines for [Header](http://httpd.apache.org/docs/current/mod/mod_headers.html#header) directives. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => { + path => '/path/to/directory', + headers => 'Set X-Robots-Tag "noindex, noarchive, nosnippet"', + }, + } +``` + +######`index_options` + +Allows configuration settings for [directory indexing](httpd.apache.org/docs/current/mod/mod_autoindex.html#indexoptions). + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + options => ['Indexes','FollowSymLinks','MultiViews'], + index_options => ['IgnoreCase', 'FancyIndexing', 'FoldersFirst', 'NameWidth=*', 'DescriptionWidth=*', 'SuppressHTMLPreamble'], + }, + ], + } +``` + +######`index_order_default` + +Sets the [default ordering](http://httpd.apache.org/docs/current/mod/mod_autoindex.html#indexorderdefault) of the directory index. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + order => 'Allow,Deny', + index_order_default => ['Descending', 'Date'], + }, + ], + } +``` + +######`options` + +Lists the [Options](httpd.apache.org/docs/current/mod/core.html#options) for the given Directory block. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + options => ['Indexes','FollowSymLinks','MultiViews'], + }, + ], + } +``` + +######`order` + +Sets the order of processing Allow and Deny statements as per [Apache core documentation](httpd.apache.org/docs/2.2/mod/mod_authz_host.html#order). **Deprecated:** This parameter is being deprecated due to a change in Apache. It will only work with Apache 2.2 and lower. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + order => 'Allow,Deny', + }, + ], + } +``` + +######`passenger_enabled` + +Sets the value for the [PassengerEnabled](http://www.modrails.com/documentation/Users%20guide%20Apache.html#PassengerEnabled) directory to 'on' or 'off'. Requires `apache::mod::passenger` to be included. + +```puppet + apache::vhost { 'sample.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + passenger_enabled => 'on', + }, + ], + } +``` + +*Note:* Be aware that there is an [issue](http://www.conandalton.net/2010/06/passengerenabled-off-not-working.html) using the PassengerEnabled directive with the PassengerHighPerformance directive. + +######`php_admin_value` and `php_admin_flag` + +`php_admin_value` sets the value of the directory, and `php_admin_flag` uses a boolean to configure the directory. Further information can be found [here](http://php.net/manual/en/configuration.changes.php). + +######`ssl_options` + +String or list of [SSLOptions](https://httpd.apache.org/docs/current/mod/mod_ssl.html#ssloptions), which configure SSL engine run-time options. This handler takes precedence over SSLOptions set in the parent block of the vhost. + +```puppet + apache::vhost { 'secure.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + ssl_options => '+ExportCertData', + }, + { path => '/path/to/different/dir', + ssl_options => [ '-StdEnvVars', '+ExportCertData'], + }, + ], + } +``` + +######`suphp` + +A hash containing the 'user' and 'group' keys for the [suPHP_UserGroup](http://www.suphp.org/DocumentationView.html?file=apache/CONFIG) setting. It must be used with `suphp_engine => on` in the vhost declaration, and may only be passed within `directories`. + +```puppet + apache::vhost { 'secure.example.net': + docroot => '/path/to/directory', + directories => [ + { path => '/path/to/directory', + suphp => + { user => 'myappuser', + group => 'myappgroup', + }, + }, + ], + } +``` + +####SSL parameters for `apache::vhost` + +All of the SSL parameters for `::vhost` will default to whatever is set in the base `apache` class. Use the below parameters to tweak individual SSL settings for specific vhosts. + +#####`ssl` + +Enables SSL for the virtual host. SSL vhosts only respond to HTTPS queries. Valid values are 'true' or 'false'. Defaults to 'false'. + +#####`ssl_ca` + +Specifies the SSL certificate authority. Defaults to 'undef'. + +#####`ssl_cert` + +Specifies the SSL certification. Defaults are based on your OS: '/etc/pki/tls/certs/localhost.crt' for RedHat, '/etc/ssl/certs/ssl-cert-snakeoil.pem' for Debian, and '/usr/local/etc/apache22/server.crt' for FreeBSD. + +#####`ssl_protocol` + +Specifies [SSLProtocol](http://httpd.apache.org/docs/current/mod/mod_ssl.html#sslprotocol). Defaults to 'undef'. + +If you do not use this parameter, it will use the HTTPD default from ssl.conf.erb, 'all -SSLv2'. + +#####`ssl_cipher` + +Specifies [SSLCipherSuite](http://httpd.apache.org/docs/current/mod/mod_ssl.html#sslciphersuite). Defaults to 'undef'. + +If you do not use this parameter, it will use the HTTPD default from ssl.conf.erb, 'HIGH:MEDIUM:!aNULL:!MD5'. + +#####`ssl_honorcipherorder` + +Sets [SSLHonorCipherOrder](http://httpd.apache.org/docs/current/mod/mod_ssl.html#sslhonorcipherorder), which is used to prefer the server's cipher preference order. Defaults to 'On' in the base `apache` config. + +#####`ssl_certs_dir` + +Specifies the location of the SSL certification directory. Defaults to '/etc/ssl/certs' on Debian, '/etc/pki/tls/certs' on RedHat, and '/usr/local/etc/apache22' on FreeBSD. + +#####`ssl_chain` + +Specifies the SSL chain. Defaults to 'undef'. (This default will work out of the box but must be updated in the base `apache` class with your specific certificate information before being used in production.) + +#####`ssl_crl` + +Specifies the certificate revocation list to use. Defaults to 'undef'. (This default will work out of the box but must be updated in the base `apache` class with your specific certificate information before being used in production.) + +#####`ssl_crl_path` + +Specifies the location of the certificate revocation list. Defaults to 'undef'. (This default will work out of the box but must be updated in the base `apache` class with your specific certificate information before being used in production.) + +#####`ssl_key` + +Specifies the SSL key. Defaults are based on your operating system: '/etc/pki/tls/private/localhost.key' for RedHat, '/etc/ssl/private/ssl-cert-snakeoil.key' for Debian, and '/usr/local/etc/apache22/server.key' for FreeBSD. (This default will work out of the box but must be updated in the base `apache` class with your specific certificate information before being used in production.) + +#####`ssl_verify_client` + +Sets the [SSLVerifyClient](http://httpd.apache.org/docs/current/mod/mod_ssl.html#sslverifyclient) directive, which sets the certificate verification level for client authentication. Valid values are: 'none', 'optional', 'require', and 'optional_no_ca'. Defaults to 'undef'. + +```puppet + apache::vhost { 'sample.example.net': + … + ssl_verify_client => 'optional', + } +``` + +#####`ssl_verify_depth` + +Sets the [SSLVerifyDepth](http://httpd.apache.org/docs/current/mod/mod_ssl.html#sslverifydepth) directive, which specifies the maximum depth of CA certificates in client certificate verification. Defaults to 'undef'. + +```puppet + apache::vhost { 'sample.example.net': + … + ssl_verify_depth => 1, + } +``` + +#####`ssl_options` + +Sets the [SSLOptions](http://httpd.apache.org/docs/current/mod/mod_ssl.html#ssloptions) directive, which configures various SSL engine run-time options. This is the global setting for the given vhost and can be a string or an array. Defaults to 'undef'. + +A string: + +```puppet + apache::vhost { 'sample.example.net': + … + ssl_options => '+ExportCertData', + } +``` + +An array: + +```puppet + apache::vhost { 'sample.example.net': + … + ssl_options => [ '+StrictRequire', '+ExportCertData' ], + } +``` + +#####`ssl_proxyengine` + +Specifies whether or not to use [SSLProxyEngine](http://httpd.apache.org/docs/current/mod/mod_ssl.html#sslproxyengine). Valid values are 'true' and 'false'. Defaults to 'false'. + + +###Virtual Host Examples + +The apache module allows you to set up pretty much any configuration of virtual host you might need. This section will address some common configurations, but look at the [Tests section](https://github.com/puppetlabs/puppetlabs-apache/tree/master/tests) for even more examples. + +Configure a vhost with a server administrator + +```puppet + apache::vhost { 'third.example.com': + port => '80', + docroot => '/var/www/third', + serveradmin => 'admin@example.com', + } +``` + +- - - + +Set up a vhost with aliased servers + +```puppet + apache::vhost { 'sixth.example.com': + serveraliases => [ + 'sixth.example.org', + 'sixth.example.net', + ], + port => '80', + docroot => '/var/www/fifth', + } +``` + +- - - + +Configure a vhost with a cgi-bin + +```puppet + apache::vhost { 'eleventh.example.com': + port => '80', + docroot => '/var/www/eleventh', + scriptalias => '/usr/lib/cgi-bin', + } +``` + +- - - + +Set up a vhost with a rack configuration + +```puppet + apache::vhost { 'fifteenth.example.com': + port => '80', + docroot => '/var/www/fifteenth', + rack_base_uris => ['/rackapp1', '/rackapp2'], + } +``` + +- - - + +Set up a mix of SSL and non-SSL vhosts at the same domain + +```puppet + #The non-ssl vhost + apache::vhost { 'first.example.com non-ssl': + servername => 'first.example.com', + port => '80', + docroot => '/var/www/first', + } + + #The SSL vhost at the same domain + apache::vhost { 'first.example.com ssl': + servername => 'first.example.com', + port => '443', + docroot => '/var/www/first', + ssl => true, + } +``` + +- - - + +Configure a vhost to redirect non-SSL connections to SSL + +```puppet + apache::vhost { 'sixteenth.example.com non-ssl': + servername => 'sixteenth.example.com', + port => '80', + docroot => '/var/www/sixteenth', + redirect_status => 'permanent' + redirect_dest => 'https://sixteenth.example.com/' + } + apache::vhost { 'sixteenth.example.com ssl': + servername => 'sixteenth.example.com', + port => '443', + docroot => '/var/www/sixteenth', + ssl => true, + } +``` + +- - - + +Set up IP-based vhosts on any listen port and have them respond to requests on specific IP addresses. In this example, we will set listening on ports 80 and 81. This is required because the example vhosts are not declared with a port parameter. + +```puppet + apache::listen { '80': } + apache::listen { '81': } +``` + +Then we will set up the IP-based vhosts + +```puppet + apache::vhost { 'first.example.com': + ip => '10.0.0.10', + docroot => '/var/www/first', + ip_based => true, + } + apache::vhost { 'second.example.com': + ip => '10.0.0.11', + docroot => '/var/www/second', + ip_based => true, + } +``` + +- - - + +Configure a mix of name-based and IP-based vhosts. First, we will add two IP-based vhosts on 10.0.0.10, one SSL and one non-SSL + +```puppet + apache::vhost { 'The first IP-based vhost, non-ssl': + servername => 'first.example.com', + ip => '10.0.0.10', + port => '80', + ip_based => true, + docroot => '/var/www/first', + } + apache::vhost { 'The first IP-based vhost, ssl': + servername => 'first.example.com', + ip => '10.0.0.10', + port => '443', + ip_based => true, + docroot => '/var/www/first-ssl', + ssl => true, + } +``` + +Then, we will add two name-based vhosts listening on 10.0.0.20 + +```puppet + apache::vhost { 'second.example.com': + ip => '10.0.0.20', + port => '80', + docroot => '/var/www/second', + } + apache::vhost { 'third.example.com': + ip => '10.0.0.20', + port => '80', + docroot => '/var/www/third', + } +``` + +If you want to add two name-based vhosts so that they will answer on either 10.0.0.10 or 10.0.0.20, you **MUST** declare `add_listen => 'false'` to disable the otherwise automatic 'Listen 80', as it will conflict with the preceding IP-based vhosts. + +```puppet + apache::vhost { 'fourth.example.com': + port => '80', + docroot => '/var/www/fourth', + add_listen => false, + } + apache::vhost { 'fifth.example.com': + port => '80', + docroot => '/var/www/fifth', + add_listen => false, + } +``` + +###Load Balancing + +####Defined Type: `apache::balancer` + +`apache::balancer` creates an Apache balancer cluster. Each balancer cluster needs one or more balancer members, which are declared with [`apache::balancermember`](#defined-type-apachebalancermember). + +One `apache::balancer` defined resource should be defined for each Apache load balanced set of servers. The `apache::balancermember` resources for all balancer members can be exported and collected on a single Apache load balancer server using exported resources. + +**Parameters within `apache::balancer`:** + +#####`name` + +Sets the balancer cluster's title. This parameter will also set the title of the conf.d file. + +#####`proxy_set` + +Configures key-value pairs as [ProxySet](http://httpd.apache.org/docs/current/mod/mod_proxy.html#proxyset) lines. Accepts a hash, and defaults to '{}'. + +#####`collect_exported` + +Determines whether or not to use exported resources. Valid values 'true' and 'false', defaults to 'true'. + +If you statically declare all of your backend servers, you should set this to 'false' to rely on existing declared balancer member resources. Also make sure to use `apache::balancermember` with array arguments. + +If you wish to dynamically declare your backend servers via [exported resources](http://docs.puppetlabs.com/guides/exported_resources.html) collected on a central node, you must set this parameter to 'true' in order to collect the exported balancer member resources that were exported by the balancer member nodes. + +If you choose not to use exported resources, all balancer members will be configured in a single puppet run. If you are using exported resources, Puppet has to run on the balanced nodes, then run on the balancer. + +####Defined Type: `apache::balancermember` + +Defines members of [mod_proxy_balancer](http://httpd.apache.org/docs/current/mod/mod_proxy_balancer.html), which will set up a balancer member inside a listening service configuration block in etc/apache/apache.cfg on the load balancer. + +**Parameters within `apache::balancermember`:** + +#####`name` + +Sets the title of the resource. This name will also set the name of the concat fragment. + +#####`balancer_cluster` + +Sets the Apache service's instance name. This must match the name of a declared `apache::balancer` resource. Required. + +#####`url` + +Specifies the URL used to contact the balancer member server. Defaults to 'http://${::fqdn}/'. + +#####`options` + +An array of [options](http://httpd.apache.org/docs/2.2/mod/mod_proxy.html#balancermember) to be specified after the URL. Accepts any key-value pairs available to [ProxyPass](http://httpd.apache.org/docs/2.2/mod/mod_proxy.html#proxypass). + +####Examples + +To load balance with exported resources, export the `balancermember` from the balancer member + +```puppet + @@apache::balancermember { "${::fqdn}-puppet00": + balancer_cluster => 'puppet00', + url => "ajp://${::fqdn}:8009" + options => ['ping=5', 'disablereuse=on', 'retry=5', 'ttl=120'], + } +``` + +Then, on the proxy server, create the balancer cluster + +```puppet + apache::balancer { 'puppet00': } +``` + +To load balance without exported resources, declare the following on the proxy + +```puppet + apache::balancer { 'puppet00': } + apache::balancermember { "${::fqdn}-puppet00": + balancer_cluster => 'puppet00', + url => "ajp://${::fqdn}:8009" + options => ['ping=5', 'disablereuse=on', 'retry=5', 'ttl=120'], + } +``` + +Then declare `apache::balancer` and `apache::balancermember` on the proxy server. + +If you need to use ProxySet in the balancer config + +```puppet + apache::balancer { 'puppet01': + proxy_set => {'stickysession' => 'JSESSIONID'}, + } +``` + +##Reference + +###Classes + +####Public Classes + +* [`apache`](#class-apache): Guides the basic setup of Apache. +* `apache::dev`: Installs Apache development libraries. (*Note:* On FreeBSD, you must declare `apache::package` or `apache` before `apache::dev`.) +* [`apache::mod::[name]`](#classes-apachemodname): Enables specific Apache HTTPD modules. + +####Private Classes + +* `apache::confd::no_accf`: Creates the no-accf.conf configuration file in conf.d, required by FreeBSD's Apache 2.4. +* `apache::default_confd_files`: Includes conf.d files for FreeBSD. +* `apache::default_mods`: Installs the Apache modules required to run the default configuration. +* `apache::package`: Installs and configures basic Apache packages. +* `apache::params`: Manages Apache parameters. +* `apache::service`: Manages the Apache daemon. + +###Defined Types + +####Public Defined Types + +* `apache::balancer`: Creates an Apache balancer cluster. +* `apache::balancermember`: Defines members of [mod_proxy_balancer](http://httpd.apache.org/docs/current/mod/mod_proxy_balancer.html). +* `apache::listen`: Based on the title, controls which ports Apache binds to for listening. Adds [Listen](http://httpd.apache.org/docs/current/bind.html) directives to ports.conf in the Apache HTTPD configuration directory. Titles take the form '', ':', or ':'. +* `apache::mod`: Used to enable arbitrary Apache HTTPD modules for which there is no specific `apache::mod::[name]` class. +* `apache::namevirtualhost`: Enables name-based hosting of a virtual host. Adds all [NameVirtualHost](http://httpd.apache.org/docs/current/vhosts/name-based.html) directives to the `ports.conf` file in the Apache HTTPD configuration directory. Titles take the form '\*', '*:', '\_default_:, '', or ':'. +* `apache::vhost`: Allows specialized configurations for virtual hosts that have requirements outside the defaults. + +####Private Defined Types + +* `apache::peruser::multiplexer`: Enables the [Peruser](http://www.freebsd.org/cgi/url.cgi?ports/www/apache22-peruser-mpm/pkg-descr) module for FreeBSD only. +* `apache::peruser::processor`: Enables the [Peruser](http://www.freebsd.org/cgi/url.cgi?ports/www/apache22-peruser-mpm/pkg-descr) module for FreeBSD only. + +###Templates + +The Apache module relies heavily on templates to enable the `vhost` and `apache::mod` defined types. These templates are built based on Facter facts around your operating system. Unless explicitly called out, most templates are not meant for configuration. + +##Limitations + +This module is CI tested on Centos 5 & 6, Ubuntu 12.04, Debian 7, and RHEL 5 & 6 platforms against both the OSS and Enterprise version of Puppet. + +The module contains support for other distributions and operating systems, such as FreeBSD and Amazon Linux, but is not formally tested on those and regressions may occur. + +##Development + +###Contributing + +Puppet Labs modules on the Puppet Forge are open projects, and community contributions are essential for keeping them great. We can’t access the huge number of platforms and myriad of hardware, software, and deployment configurations that Puppet is intended to serve. + +We want to keep it as easy as possible to contribute changes so that our modules work in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. + +You can read the complete module contribution guide [on the Puppet Labs wiki.](http://projects.puppetlabs.com/projects/module-site/wiki/Module_contributing) + +###Running tests + +This project contains tests for both [rspec-puppet](http://rspec-puppet.com/) and [beaker-rspec](https://github.com/puppetlabs/beaker-rspec) to verify functionality. For in-depth information please see their respective documentation. + +Quickstart: + + gem install bundler + bundle install + bundle exec rake spec + bundle exec rspec spec/acceptance + RS_DEBUG=yes bundle exec rspec spec/acceptance diff --git a/apache/README.passenger.md b/apache/README.passenger.md new file mode 100644 index 000000000..cecacccc4 --- /dev/null +++ b/apache/README.passenger.md @@ -0,0 +1,93 @@ +# Passenger + +Just enabling the Passenger module is insufficient for the use of Passenger in production. Passenger should be tunable to better fit the environment in which it is run while being aware of the resources it required. + +To this end the Apache passenger module has been modified to apply system wide Passenger tuning declarations to `passenger.conf`. Declarations specific to a virtual host should be passed through when defining a `vhost` (e.g. `rack_base_uris' parameter on the `apache::vhost` class, check `README.md`). + +# Parameters for `apache::mod::passenger` + +The following declarations are supported and can be passed to `apache::mod::passenger` as parameters, for example: + +``` +class {'apache::mod::passenger': + passenger_high_performance => 'on', + rails_autodetect => 'off', +} +``` + +The general form is using the all lower case version of the declaration. + +If you pass a default value to `apache::mod::passenger` it will be ignored and not passed through to the configuration file. + +## passenger_high_performance + +Default is `off`, when turned `on` Passenger runs in a higher performance mode that can be less compatible with other Apache modules. + +http://www.modrails.com/documentation/Users%20guide%20Apache.html#PassengerHighPerformance + +## passenger_max_pool_size + +Set's the maximum number of Passenger application processes that may simultaneously run. The default value is 6. + +http://www.modrails.com/documentation/Users%20guide%20Apache.html#_passengermaxpoolsize_lt_integer_gt + +## passenger_pool_idle_time + +The maximum number of seconds a Passenger Application process will be allowed to remain idle before being shut down. The default value is 300. + +http://www.modrails.com/documentation/Users%20guide%20Apache.html#PassengerPoolIdleTime + +## passenger_max_requests + +The maximum number of request a Passenger application will process before being restarted. The default value is 0, which indicates that a process will only shut down if the Pool Idle Time (see above) expires. + +http://www.modrails.com/documentation/Users%20guide%20Apache.html#PassengerMaxRequests + +## passenger_stat_throttle_rate + +Sets how often Passenger performs file system checks, at most once every _x_ seconds. Default is 0, which means the checks are performed with every request. + +http://www.modrails.com/documentation/Users%20guide%20Apache.html#_passengerstatthrottlerate_lt_integer_gt + +## rack_autodetect + +Should Passenger automatically detect if the document root of a virtual host is a Rack application. The default is `on` + +http://www.modrails.com/documentation/Users%20guide%20Apache.html#_rackautodetect_lt_on_off_gt + +## rails_autodetect + +Should Passenger automatically detect if the document root of a virtual host is a Rails application. The default is on. + +http://www.modrails.com/documentation/Users%20guide%20Apache.html#_railsautodetect_lt_on_off_gt + +## passenger_use_global_queue + +Allows toggling of PassengerUseGlobalQueue. NOTE: PassengerUseGlobalQueue is the default in Passenger 4.x and the versions >= 4.x have disabled this configuration option altogether. Use with caution. + +# Attribution + +The Passenger tuning parameters for the `apache::mod::puppet` Puppet class was modified by Aaron Hicks (hicksa@landcareresearch.co.nz) for work on the NeSI Project and the Tuakiri New Zealand Access Federation as a fork from the PuppetLabs Apache module on GitHub. + +* https://github.com/puppetlabs/puppetlabs-apache +* https://github.com/nesi/puppetlabs-apache +* http://www.nesi.org.nz// +* https://tuakiri.ac.nz/confluence/display/Tuakiri/Home + +# Copyright and License + +Copyright (C) 2012 [Puppet Labs](https://www.puppetlabs.com/) Inc + +Puppet Labs can be contacted at: info@puppetlabs.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/apache/Rakefile b/apache/Rakefile new file mode 100644 index 000000000..5868545f2 --- /dev/null +++ b/apache/Rakefile @@ -0,0 +1,10 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_inherits_from_params_class') +PuppetLint.configuration.send('disable_class_parameter_defaults') +PuppetLint.configuration.send('disable_documentation') +PuppetLint.configuration.send('disable_single_quote_string_with_variables') +PuppetLint.configuration.ignore_paths = ["spec/**/*.pp", "pkg/**/*.pp"] diff --git a/apache/files/httpd b/apache/files/httpd new file mode 100644 index 000000000..d65a8d445 --- /dev/null +++ b/apache/files/httpd @@ -0,0 +1,24 @@ +# Configuration file for the httpd service. + +# +# The default processing model (MPM) is the process-based +# 'prefork' model. A thread-based model, 'worker', is also +# available, but does not work with some modules (such as PHP). +# The service must be stopped before changing this variable. +# +#HTTPD=/usr/sbin/httpd.worker + +# +# To pass additional options (for instance, -D definitions) to the +# httpd binary at startup, set OPTIONS here. +# +#OPTIONS= +#OPTIONS=-DDOWN + +# +# By default, the httpd process is started in the C locale; to +# change the locale in which the server runs, the HTTPD_LANG +# variable can be set. +# +#HTTPD_LANG=C +export SHORTHOST=`hostname -s` diff --git a/apache/lib/puppet/provider/a2mod.rb b/apache/lib/puppet/provider/a2mod.rb new file mode 100644 index 000000000..670aca3d0 --- /dev/null +++ b/apache/lib/puppet/provider/a2mod.rb @@ -0,0 +1,34 @@ +class Puppet::Provider::A2mod < Puppet::Provider + def self.prefetch(mods) + instances.each do |prov| + if mod = mods[prov.name] + mod.provider = prov + end + end + end + + def flush + @property_hash.clear + end + + def properties + if @property_hash.empty? + @property_hash = query || {:ensure => :absent} + @property_hash[:ensure] = :absent if @property_hash.empty? + end + @property_hash.dup + end + + def query + self.class.instances.each do |mod| + if mod.name == self.name or mod.name.downcase == self.name + return mod.properties + end + end + nil + end + + def exists? + properties[:ensure] != :absent + end +end diff --git a/apache/lib/puppet/provider/a2mod/a2mod.rb b/apache/lib/puppet/provider/a2mod/a2mod.rb new file mode 100644 index 000000000..e257a579e --- /dev/null +++ b/apache/lib/puppet/provider/a2mod/a2mod.rb @@ -0,0 +1,35 @@ +require 'puppet/provider/a2mod' + +Puppet::Type.type(:a2mod).provide(:a2mod, :parent => Puppet::Provider::A2mod) do + desc "Manage Apache 2 modules on Debian and Ubuntu" + + optional_commands :encmd => "a2enmod" + optional_commands :discmd => "a2dismod" + commands :apache2ctl => "apache2ctl" + + confine :osfamily => :debian + defaultfor :operatingsystem => [:debian, :ubuntu] + + def self.instances + modules = apache2ctl("-M").lines.collect { |line| + m = line.match(/(\w+)_module \(shared\)$/) + m[1] if m + }.compact + + modules.map do |mod| + new( + :name => mod, + :ensure => :present, + :provider => :a2mod + ) + end + end + + def create + encmd resource[:name] + end + + def destroy + discmd resource[:name] + end +end diff --git a/apache/lib/puppet/provider/a2mod/gentoo.rb b/apache/lib/puppet/provider/a2mod/gentoo.rb new file mode 100644 index 000000000..07319dfdc --- /dev/null +++ b/apache/lib/puppet/provider/a2mod/gentoo.rb @@ -0,0 +1,116 @@ +require 'puppet/util/filetype' +Puppet::Type.type(:a2mod).provide(:gentoo, :parent => Puppet::Provider) do + desc "Manage Apache 2 modules on Gentoo" + + confine :operatingsystem => :gentoo + defaultfor :operatingsystem => :gentoo + + attr_accessor :property_hash + + def create + @property_hash[:ensure] = :present + end + + def exists? + (!(@property_hash[:ensure].nil?) and @property_hash[:ensure] == :present) + end + + def destroy + @property_hash[:ensure] = :absent + end + + def flush + self.class.flush + end + + class << self + attr_reader :conf_file + end + + def self.clear + @mod_resources = [] + @modules = [] + @other_args = "" + end + + def self.initvars + @conf_file = "/etc/conf.d/apache2" + @filetype = Puppet::Util::FileType.filetype(:flat).new(conf_file) + @mod_resources = [] + @modules = [] + @other_args = "" + end + + self.initvars + + # Retrieve an array of all existing modules + def self.modules + if @modules.length <= 0 + # Locate the APACHE_OPTS variable + records = filetype.read.split(/\n/) + apache2_opts = records.grep(/^\s*APACHE2_OPTS=/).first + + # Extract all defines + while apache2_opts.sub!(/-D\s+(\w+)/, '') + @modules << $1.downcase + end + + # Hang on to any remaining options. + if apache2_opts.match(/APACHE2_OPTS="(.+)"/) + @other_args = $1.strip + end + + @modules.sort!.uniq! + end + + @modules + end + + def self.prefetch(resources={}) + # Match resources with existing providers + instances.each do |provider| + if resource = resources[provider.name] + resource.provider = provider + end + end + + # Store all resources using this provider for flushing + resources.each do |name, resource| + @mod_resources << resource + end + end + + def self.instances + modules.map {|mod| new(:name => mod, :provider => :gentoo, :ensure => :present)} + end + + def self.flush + + mod_list = modules + mods_to_remove = @mod_resources.select {|mod| mod.should(:ensure) == :absent}.map {|mod| mod[:name]} + mods_to_add = @mod_resources.select {|mod| mod.should(:ensure) == :present}.map {|mod| mod[:name]} + + mod_list -= mods_to_remove + mod_list += mods_to_add + mod_list.sort!.uniq! + + if modules != mod_list + opts = @other_args + " " + opts << mod_list.map {|mod| "-D #{mod.upcase}"}.join(" ") + opts.strip! + opts.gsub!(/\s+/, ' ') + + apache2_opts = %Q{APACHE2_OPTS="#{opts}"} + Puppet.debug("Writing back \"#{apache2_opts}\" to #{conf_file}") + + records = filetype.read.split(/\n/) + + opts_index = records.find_index {|i| i.match(/^\s*APACHE2_OPTS/)} + records[opts_index] = apache2_opts + + filetype.backup + filetype.write(records.join("\n")) + @modules = mod_list + end + end +end diff --git a/apache/lib/puppet/provider/a2mod/modfix.rb b/apache/lib/puppet/provider/a2mod/modfix.rb new file mode 100644 index 000000000..8f35b2e4a --- /dev/null +++ b/apache/lib/puppet/provider/a2mod/modfix.rb @@ -0,0 +1,12 @@ +Puppet::Type.type(:a2mod).provide :modfix do + desc "Dummy provider for A2mod. + + Fake nil resources when there is no crontab binary available. Allows + puppetd to run on a bootstrapped machine before a Cron package has been + installed. Workaround for: http://projects.puppetlabs.com/issues/2384 + " + + def self.instances + [] + end +end \ No newline at end of file diff --git a/apache/lib/puppet/provider/a2mod/redhat.rb b/apache/lib/puppet/provider/a2mod/redhat.rb new file mode 100644 index 000000000..ea5494cb4 --- /dev/null +++ b/apache/lib/puppet/provider/a2mod/redhat.rb @@ -0,0 +1,60 @@ +require 'puppet/provider/a2mod' + +Puppet::Type.type(:a2mod).provide(:redhat, :parent => Puppet::Provider::A2mod) do + desc "Manage Apache 2 modules on RedHat family OSs" + + commands :apachectl => "apachectl" + + confine :osfamily => :redhat + defaultfor :osfamily => :redhat + + require 'pathname' + + # modpath: Path to default apache modules directory /etc/httpd/mod.d + # modfile: Path to module load configuration file; Default: resides under modpath directory + # libfile: Path to actual apache module library. Added in modfile LoadModule + + attr_accessor :modfile, :libfile + class << self + attr_accessor :modpath + def preinit + @modpath = "/etc/httpd/mod.d" + end + end + + self.preinit + + def create + File.open(modfile,'w') do |f| + f.puts "LoadModule #{resource[:identifier]} #{libfile}" + end + end + + def destroy + File.delete(modfile) + end + + def self.instances + modules = apachectl("-M").lines.collect { |line| + m = line.match(/(\w+)_module \(shared\)$/) + m[1] if m + }.compact + + modules.map do |mod| + new( + :name => mod, + :ensure => :present, + :provider => :redhat + ) + end + end + + def modfile + modfile ||= "#{self.class.modpath}/#{resource[:name]}.load" + end + + # Set libfile path: If absolute path is passed, then maintain it. Else, make it default from 'modules' dir. + def libfile + libfile = Pathname.new(resource[:lib]).absolute? ? resource[:lib] : "modules/#{resource[:lib]}" + end +end diff --git a/apache/lib/puppet/type/a2mod.rb b/apache/lib/puppet/type/a2mod.rb new file mode 100644 index 000000000..07a911e5e --- /dev/null +++ b/apache/lib/puppet/type/a2mod.rb @@ -0,0 +1,30 @@ +Puppet::Type.newtype(:a2mod) do + @doc = "Manage Apache 2 modules" + + ensurable + + newparam(:name) do + Puppet.warning "The a2mod provider is deprecated, please use apache::mod instead" + desc "The name of the module to be managed" + + isnamevar + + end + + newparam(:lib) do + desc "The name of the .so library to be loaded" + + defaultto { "mod_#{@resource[:name]}.so" } + end + + newparam(:identifier) do + desc "Module identifier string used by LoadModule. Default: module-name_module" + + # http://httpd.apache.org/docs/2.2/mod/module-dict.html#ModuleIdentifier + + defaultto { "#{resource[:name]}_module" } + end + + autorequire(:package) { catalog.resource(:package, 'httpd')} + +end diff --git a/apache/manifests/balancer.pp b/apache/manifests/balancer.pp new file mode 100644 index 000000000..30887823b --- /dev/null +++ b/apache/manifests/balancer.pp @@ -0,0 +1,80 @@ +# == Define Resource Type: apache::balancer +# +# This type will create an apache balancer cluster file inside the conf.d +# directory. Each balancer cluster needs one or more balancer members (that can +# be declared with the apache::balancermember defined resource type). Using +# storeconfigs, you can export the apache::balancermember resources on all +# balancer members, and then collect them on a single apache load balancer +# server. +# +# === Requirement/Dependencies: +# +# Currently requires the puppetlabs/concat module on the Puppet Forge and uses +# storeconfigs on the Puppet Master to export/collect resources from all +# balancer members. +# +# === Parameters +# +# [*name*] +# The namevar of the defined resource type is the balancer clusters name. +# This name is also used in the name of the conf.d file +# +# [*proxy_set*] +# Hash, default empty. If given, each key-value pair will be used as a ProxySet +# line in the configuration. +# +# [*collect_exported*] +# Boolean, default 'true'. True means 'collect exported @@balancermember +# resources' (for the case when every balancermember node exports itself), +# false means 'rely on the existing declared balancermember resources' (for the +# case when you know the full set of balancermembers in advance and use +# apache::balancermember with array arguments, which allows you to deploy +# everything in 1 run) +# +# +# === Examples +# +# Exporting the resource for a balancer member: +# +# apache::balancer { 'puppet00': } +# +define apache::balancer ( + $proxy_set = {}, + $collect_exported = true, +) { + include concat::setup + include ::apache::mod::proxy_balancer + + $target = "${::apache::params::confd_dir}/balancer_${name}.conf" + + concat { $target: + owner => '0', + group => '0', + mode => '0644', + notify => Service['httpd'], + } + + concat::fragment { "00-${name}-header": + target => $target, + order => '01', + content => "\n", + } + + if $collect_exported { + Apache::Balancermember <<| balancer_cluster == $name |>> + } + # else: the resources have been created and they introduced their + # concat fragments. We don't have to do anything about them. + + concat::fragment { "01-${name}-proxyset": + target => $target, + order => '19', + content => inline_template("<% proxy_set.each do |key, value| %> Proxyset <%= key %>=<%= value %>\n<% end %>"), + } + + concat::fragment { "01-${name}-footer": + target => $target, + order => '20', + content => "\n", + } +} diff --git a/apache/manifests/balancermember.pp b/apache/manifests/balancermember.pp new file mode 100644 index 000000000..c48cb1ebb --- /dev/null +++ b/apache/manifests/balancermember.pp @@ -0,0 +1,52 @@ +# == Define Resource Type: apache::balancermember +# +# This type will setup a balancer member inside a listening service +# configuration block in /etc/apache/apache.cfg on the load balancer. +# currently it only has the ability to specify the instance name, url and an +# array of options. More features can be added as needed. The best way to +# implement this is to export this resource for all apache balancer member +# servers, and then collect them on the main apache load balancer. +# +# === Requirement/Dependencies: +# +# Currently requires the puppetlabs/concat module on the Puppet Forge and +# uses storeconfigs on the Puppet Master to export/collect resources +# from all balancer members. +# +# === Parameters +# +# [*name*] +# The title of the resource is arbitrary and only utilized in the concat +# fragment name. +# +# [*balancer_cluster*] +# The apache service's instance name (or, the title of the apache::balancer +# resource). This must match up with a declared apache::balancer resource. +# +# [*url*] +# The url used to contact the balancer member server. +# +# [*options*] +# An array of options to be specified after the url. +# +# === Examples +# +# Exporting the resource for a balancer member: +# +# @@apache::balancermember { 'apache': +# balancer_cluster => 'puppet00', +# url => "ajp://${::fqdn}:8009" +# options => ['ping=5', 'disablereuse=on', 'retry=5', 'ttl=120'], +# } +# +define apache::balancermember( + $balancer_cluster, + $url = "http://${::fqdn}/", + $options = [], +) { + + concat::fragment { "BalancerMember ${url}": + target => "${::apache::params::confd_dir}/balancer_${balancer_cluster}.conf", + content => inline_template(" BalancerMember ${url} <%= @options.join ' ' %>\n"), + } +} diff --git a/apache/manifests/confd/no_accf.pp b/apache/manifests/confd/no_accf.pp new file mode 100644 index 000000000..f35c0c8b9 --- /dev/null +++ b/apache/manifests/confd/no_accf.pp @@ -0,0 +1,10 @@ +class apache::confd::no_accf { + # Template uses no variables + file { 'no-accf.conf': + ensure => 'file', + path => "${::apache::confd_dir}/no-accf.conf", + content => template('apache/confd/no-accf.conf.erb'), + require => Exec["mkdir ${::apache::confd_dir}"], + before => File[$::apache::confd_dir], + } +} diff --git a/apache/manifests/default_confd_files.pp b/apache/manifests/default_confd_files.pp new file mode 100644 index 000000000..c06b30c83 --- /dev/null +++ b/apache/manifests/default_confd_files.pp @@ -0,0 +1,15 @@ +class apache::default_confd_files ( + $all = true, +) { + # The rest of the conf.d/* files only get loaded if we want them + if $all { + case $::osfamily { + 'freebsd': { + include ::apache::confd::no_accf + } + default: { + # do nothing + } + } + } +} diff --git a/apache/manifests/default_mods.pp b/apache/manifests/default_mods.pp new file mode 100644 index 000000000..139503e3c --- /dev/null +++ b/apache/manifests/default_mods.pp @@ -0,0 +1,154 @@ +class apache::default_mods ( + $all = true, + $mods = undef, + $apache_version = $::apache::apache_version +) { + # These are modules required to run the default configuration. + # They are not configurable at this time, so we just include + # them to make sure it works. + case $::osfamily { + 'redhat', 'freebsd': { + ::apache::mod { 'log_config': } + } + default: {} + } + ::apache::mod { 'authz_host': } + + # The rest of the modules only get loaded if we want all modules enabled + if $all { + case $::osfamily { + 'debian': { + include ::apache::mod::reqtimeout + } + 'redhat': { + include ::apache::mod::cache + include ::apache::mod::mime + include ::apache::mod::mime_magic + include ::apache::mod::vhost_alias + include ::apache::mod::rewrite + ::apache::mod { 'actions': } + ::apache::mod { 'auth_digest': } + ::apache::mod { 'authn_anon': } + ::apache::mod { 'authn_dbm': } + ::apache::mod { 'authz_dbm': } + ::apache::mod { 'authz_owner': } + ::apache::mod { 'expires': } + ::apache::mod { 'ext_filter': } + ::apache::mod { 'include': } + ::apache::mod { 'logio': } + ::apache::mod { 'speling': } + ::apache::mod { 'substitute': } + ::apache::mod { 'suexec': } + ::apache::mod { 'usertrack': } + ::apache::mod { 'version': } + + if $apache_version >= 2.4 { + # Lets fork it + ::apache::mod { 'systemd': } + + ::apache::mod { 'unixd': } + ::apache::mod { 'authn_core': } + } + else { + ::apache::mod { 'authn_alias': } + ::apache::mod { 'authn_default': } + } + } + 'freebsd': { + include ::apache::mod::cache + include ::apache::mod::disk_cache + include ::apache::mod::headers + include ::apache::mod::info + include ::apache::mod::mime_magic + include ::apache::mod::reqtimeout + include ::apache::mod::rewrite + include ::apache::mod::userdir + include ::apache::mod::vhost_alias + + ::apache::mod { 'actions': } + ::apache::mod { 'asis': } + ::apache::mod { 'auth_digest': } + ::apache::mod { 'authn_alias': } + ::apache::mod { 'authn_anon': } + ::apache::mod { 'authn_dbm': } + ::apache::mod { 'authn_default': } + ::apache::mod { 'authz_dbm': } + ::apache::mod { 'authz_owner': } + ::apache::mod { 'cern_meta': } + ::apache::mod { 'charset_lite': } + ::apache::mod { 'dumpio': } + ::apache::mod { 'expires': } + ::apache::mod { 'file_cache': } + ::apache::mod { 'filter':} + ::apache::mod { 'imagemap':} + ::apache::mod { 'include': } + ::apache::mod { 'logio': } + ::apache::mod { 'speling': } + ::apache::mod { 'unique_id': } + ::apache::mod { 'usertrack': } + ::apache::mod { 'version': } + } + default: {} + } + case $::apache::mpm_module { + 'prefork': { + include ::apache::mod::cgi + } + 'worker': { + include ::apache::mod::cgid + } + default: { + # do nothing + } + } + include ::apache::mod::alias + include ::apache::mod::autoindex + include ::apache::mod::dav + include ::apache::mod::dav_fs + include ::apache::mod::deflate + include ::apache::mod::dir + include ::apache::mod::mime + include ::apache::mod::negotiation + include ::apache::mod::setenvif + ::apache::mod { 'auth_basic': } + ::apache::mod { 'authn_file': } + + if $apache_version >= 2.4 { + # authz_core is needed for 'Require' directive + ::apache::mod { 'authz_core': + id => 'authz_core_module', + } + + # filter is needed by mod_deflate + ::apache::mod { 'filter': } + } else { + ::apache::mod { 'authz_default': } + } + + ::apache::mod { 'authz_groupfile': } + ::apache::mod { 'authz_user': } + ::apache::mod { 'env': } + } elsif $mods { + ::apache::default_mods::load { $mods: } + + if $apache_version >= 2.4 { + # authz_core is needed for 'Require' directive + ::apache::mod { 'authz_core': + id => 'authz_core_module', + } + + # filter is needed by mod_deflate + ::apache::mod { 'filter': } + } + } else { + if $apache_version >= 2.4 { + # authz_core is needed for 'Require' directive + ::apache::mod { 'authz_core': + id => 'authz_core_module', + } + + # filter is needed by mod_deflate + ::apache::mod { 'filter': } + } + } +} diff --git a/apache/manifests/default_mods/load.pp b/apache/manifests/default_mods/load.pp new file mode 100644 index 000000000..356e9fa00 --- /dev/null +++ b/apache/manifests/default_mods/load.pp @@ -0,0 +1,8 @@ +# private define +define apache::default_mods::load ($module = $title) { + if defined("apache::mod::${module}") { + include "::apache::mod::${module}" + } else { + ::apache::mod { $module: } + } +} diff --git a/apache/manifests/dev.pp b/apache/manifests/dev.pp new file mode 100644 index 000000000..4eaeb5578 --- /dev/null +++ b/apache/manifests/dev.pp @@ -0,0 +1,11 @@ +class apache::dev { + if $::osfamily == 'FreeBSD' and !defined(Class['apache::package']) { + fail('apache::dev requires apache::package; please include apache or apache::package class first') + } + include ::apache::params + $packages = $::apache::params::dev_packages + package { $packages: + ensure => present, + require => Package['httpd'], + } +} diff --git a/apache/manifests/init.pp b/apache/manifests/init.pp new file mode 100644 index 000000000..7f2565cf9 --- /dev/null +++ b/apache/manifests/init.pp @@ -0,0 +1,336 @@ +# Class: apache +# +# This class installs Apache +# +# Parameters: +# +# Actions: +# - Install Apache +# - Manage Apache service +# +# Requires: +# +# Sample Usage: +# +class apache ( + $service_name = $::apache::params::service_name, + $default_mods = true, + $default_vhost = true, + $default_confd_files = true, + $default_ssl_vhost = false, + $default_ssl_cert = $::apache::params::default_ssl_cert, + $default_ssl_key = $::apache::params::default_ssl_key, + $default_ssl_chain = undef, + $default_ssl_ca = undef, + $default_ssl_crl_path = undef, + $default_ssl_crl = undef, + $ip = undef, + $service_enable = true, + $service_ensure = 'running', + $purge_configs = true, + $purge_vdir = false, + $serveradmin = 'root@localhost', + $sendfile = 'On', + $error_documents = false, + $timeout = '120', + $httpd_dir = $::apache::params::httpd_dir, + $server_root = $::apache::params::server_root, + $confd_dir = $::apache::params::confd_dir, + $vhost_dir = $::apache::params::vhost_dir, + $vhost_enable_dir = $::apache::params::vhost_enable_dir, + $mod_dir = $::apache::params::mod_dir, + $mod_enable_dir = $::apache::params::mod_enable_dir, + $mpm_module = $::apache::params::mpm_module, + $conf_template = $::apache::params::conf_template, + $servername = $::apache::params::servername, + $manage_user = true, + $manage_group = true, + $user = $::apache::params::user, + $group = $::apache::params::group, + $keepalive = $::apache::params::keepalive, + $keepalive_timeout = $::apache::params::keepalive_timeout, + $logroot = $::apache::params::logroot, + $log_level = $::apache::params::log_level, + $ports_file = $::apache::params::ports_file, + $apache_version = $::apache::version::default, + $server_tokens = 'OS', + $server_signature = 'On', + $trace_enable = 'On', + $package_ensure = 'installed', +) inherits ::apache::params { + validate_bool($default_vhost) + validate_bool($default_ssl_vhost) + validate_bool($default_confd_files) + # true/false is sufficient for both ensure and enable + validate_bool($service_enable) + + $valid_mpms_re = $apache_version ? { + 2.4 => '(event|itk|peruser|prefork|worker)', + default => '(event|itk|prefork|worker)' + } + + if $mpm_module { + validate_re($mpm_module, $valid_mpms_re) + } + + # NOTE: on FreeBSD it's mpm module's responsibility to install httpd package. + # NOTE: the same strategy may be introduced for other OSes. For this, you + # should delete the 'if' block below and modify all MPM modules' manifests + # such that they include apache::package class (currently event.pp, itk.pp, + # peruser.pp, prefork.pp, worker.pp). + if $::osfamily != 'FreeBSD' { + package { 'httpd': + ensure => $package_ensure, + name => $::apache::params::apache_name, + notify => Class['Apache::Service'], + } + } + validate_re($sendfile, [ '^[oO]n$' , '^[oO]ff$' ]) + + # declare the web server user and group + # Note: requiring the package means the package ought to create them and not puppet + validate_bool($manage_user) + if $manage_user { + user { $user: + ensure => present, + gid => $group, + require => Package['httpd'], + } + } + validate_bool($manage_group) + if $manage_group { + group { $group: + ensure => present, + require => Package['httpd'] + } + } + + $valid_log_level_re = '(emerg|alert|crit|error|warn|notice|info|debug)' + + validate_re($log_level, $valid_log_level_re, + "Log level '${log_level}' is not one of the supported Apache HTTP Server log levels.") + + class { '::apache::service': + service_name => $service_name, + service_enable => $service_enable, + service_ensure => $service_ensure, + } + + # Deprecated backwards-compatibility + if $purge_vdir { + warning('Class[\'apache\'] parameter purge_vdir is deprecated in favor of purge_configs') + $purge_confd = $purge_vdir + } else { + $purge_confd = $purge_configs + } + + Exec { + path => '/bin:/sbin:/usr/bin:/usr/sbin', + } + + exec { "mkdir ${confd_dir}": + creates => $confd_dir, + require => Package['httpd'], + } + file { $confd_dir: + ensure => directory, + recurse => true, + purge => $purge_confd, + notify => Class['Apache::Service'], + require => Package['httpd'], + } + + if ! defined(File[$mod_dir]) { + exec { "mkdir ${mod_dir}": + creates => $mod_dir, + require => Package['httpd'], + } + # Don't purge available modules if an enable dir is used + $purge_mod_dir = $purge_configs and !$mod_enable_dir + file { $mod_dir: + ensure => directory, + recurse => true, + purge => $purge_mod_dir, + notify => Class['Apache::Service'], + require => Package['httpd'], + } + } + + if $mod_enable_dir and ! defined(File[$mod_enable_dir]) { + $mod_load_dir = $mod_enable_dir + exec { "mkdir ${mod_enable_dir}": + creates => $mod_enable_dir, + require => Package['httpd'], + } + file { $mod_enable_dir: + ensure => directory, + recurse => true, + purge => $purge_configs, + notify => Class['Apache::Service'], + require => Package['httpd'], + } + } else { + $mod_load_dir = $mod_dir + } + + if ! defined(File[$vhost_dir]) { + exec { "mkdir ${vhost_dir}": + creates => $vhost_dir, + require => Package['httpd'], + } + file { $vhost_dir: + ensure => directory, + recurse => true, + purge => $purge_configs, + notify => Class['Apache::Service'], + require => Package['httpd'], + } + } + + if $vhost_enable_dir and ! defined(File[$vhost_enable_dir]) { + $vhost_load_dir = $vhost_enable_dir + exec { "mkdir ${vhost_load_dir}": + creates => $vhost_load_dir, + require => Package['httpd'], + } + file { $vhost_enable_dir: + ensure => directory, + recurse => true, + purge => $purge_configs, + notify => Class['Apache::Service'], + require => Package['httpd'], + } + } else { + $vhost_load_dir = $vhost_dir + } + + concat { $ports_file: + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + notify => Class['Apache::Service'], + require => Package['httpd'], + } + concat::fragment { 'Apache ports header': + target => $ports_file, + content => template('apache/ports_header.erb') + } + + if $::apache::params::conf_dir and $::apache::params::conf_file { + case $::osfamily { + 'debian': { + $docroot = '/var/www' + $pidfile = '${APACHE_PID_FILE}' + $error_log = 'error.log' + $error_documents_path = '/usr/share/apache2/error' + $scriptalias = '/usr/lib/cgi-bin' + $access_log_file = 'access.log' + } + 'redhat': { + $docroot = '/var/www/html' + $pidfile = 'run/httpd.pid' + $error_log = 'error_log' + $error_documents_path = '/var/www/error' + $scriptalias = '/var/www/cgi-bin' + $access_log_file = 'access_log' + } + 'freebsd': { + $docroot = '/usr/local/www/apache22/data' + $pidfile = '/var/run/httpd.pid' + $error_log = 'httpd-error.log' + $error_documents_path = '/usr/local/www/apache22/error' + $scriptalias = '/usr/local/www/apache22/cgi-bin' + $access_log_file = 'httpd-access.log' + } + default: { + fail("Unsupported osfamily ${::osfamily}") + } + } + + $apxs_workaround = $::osfamily ? { + 'freebsd' => true, + default => false + } + + # Template uses: + # - $pidfile + # - $user + # - $group + # - $logroot + # - $error_log + # - $sendfile + # - $mod_dir + # - $ports_file + # - $confd_dir + # - $vhost_dir + # - $error_documents + # - $error_documents_path + # - $apxs_workaround + # - $keepalive + # - $keepalive_timeout + # - $server_root + # - $server_tokens + # - $server_signature + # - $trace_enable + file { "${::apache::params::conf_dir}/${::apache::params::conf_file}": + ensure => file, + content => template($conf_template), + notify => Class['Apache::Service'], + require => Package['httpd'], + } + + # preserve back-wards compatibility to the times when default_mods was + # only a boolean value. Now it can be an array (too) + if is_array($default_mods) { + class { '::apache::default_mods': + all => false, + mods => $default_mods, + } + } else { + class { '::apache::default_mods': + all => $default_mods, + } + } + class { '::apache::default_confd_files': + all => $default_confd_files + } + if $mpm_module { + class { "::apache::mod::${mpm_module}": } + } + + $default_vhost_ensure = $default_vhost ? { + true => 'present', + false => 'absent' + } + $default_ssl_vhost_ensure = $default_ssl_vhost ? { + true => 'present', + false => 'absent' + } + + ::apache::vhost { 'default': + ensure => $default_vhost_ensure, + port => 80, + docroot => $docroot, + scriptalias => $scriptalias, + serveradmin => $serveradmin, + access_log_file => $access_log_file, + priority => '15', + ip => $ip, + } + $ssl_access_log_file = $::osfamily ? { + 'freebsd' => $access_log_file, + default => "ssl_${access_log_file}", + } + ::apache::vhost { 'default-ssl': + ensure => $default_ssl_vhost_ensure, + port => 443, + ssl => true, + docroot => $docroot, + scriptalias => $scriptalias, + serveradmin => $serveradmin, + access_log_file => $ssl_access_log_file, + priority => '15', + ip => $ip, + } + } +} diff --git a/apache/manifests/listen.pp b/apache/manifests/listen.pp new file mode 100644 index 000000000..503ee8860 --- /dev/null +++ b/apache/manifests/listen.pp @@ -0,0 +1,9 @@ +define apache::listen { + $listen_addr_port = $name + + # Template uses: $listen_addr_port + concat::fragment { "Listen ${listen_addr_port}": + target => $::apache::ports_file, + content => template('apache/listen.erb'), + } +} diff --git a/apache/manifests/mod.pp b/apache/manifests/mod.pp new file mode 100644 index 000000000..8be99afd0 --- /dev/null +++ b/apache/manifests/mod.pp @@ -0,0 +1,120 @@ +define apache::mod ( + $package = undef, + $package_ensure = 'present', + $lib = undef, + $lib_path = $::apache::params::lib_path, + $id = undef, + $path = undef, +) { + if ! defined(Class['apache']) { + fail('You must include the apache base class before using any apache defined resources') + } + + $mod = $name + #include apache #This creates duplicate resources in rspec-puppet + $mod_dir = $::apache::mod_dir + + # Determine if we have special lib + $mod_libs = $::apache::params::mod_libs + $mod_lib = $mod_libs[$mod] # 2.6 compatibility hack + if $lib { + $_lib = $lib + } elsif $mod_lib { + $_lib = $mod_lib + } else { + $_lib = "mod_${mod}.so" + } + + # Determine if declaration specified a path to the module + if $path { + $_path = $path + } else { + $_path = "${lib_path}/${_lib}" + } + + if $id { + $_id = $id + } else { + $_id = "${mod}_module" + } + + # Determine if we have a package + $mod_packages = $::apache::params::mod_packages + $mod_package = $mod_packages[$mod] # 2.6 compatibility hack + if $package { + $_package = $package + } elsif $mod_package { + $_package = $mod_package + } + if $_package and ! defined(Package[$_package]) { + # note: FreeBSD/ports uses apxs tool to activate modules; apxs clutters + # httpd.conf with 'LoadModule' directives; here, by proper resource + # ordering, we ensure that our version of httpd.conf is reverted after + # the module gets installed. + $package_before = $::osfamily ? { + 'freebsd' => [ + File["${mod_dir}/${mod}.load"], + File["${::apache::params::conf_dir}/${::apache::params::conf_file}"] + ], + default => File["${mod_dir}/${mod}.load"], + } + # $_package may be an array + package { $_package: + ensure => $package_ensure, + require => Package['httpd'], + before => $package_before, + } + } + + file { "${mod}.load": + ensure => file, + path => "${mod_dir}/${mod}.load", + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + content => "LoadModule ${_id} ${_path}\n", + require => [ + Package['httpd'], + Exec["mkdir ${mod_dir}"], + ], + before => File[$mod_dir], + notify => Service['httpd'], + } + + if $::osfamily == 'Debian' { + $enable_dir = $::apache::mod_enable_dir + file{ "${mod}.load symlink": + ensure => link, + path => "${enable_dir}/${mod}.load", + target => "${mod_dir}/${mod}.load", + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + require => [ + File["${mod}.load"], + Exec["mkdir ${enable_dir}"], + ], + before => File[$enable_dir], + notify => Service['httpd'], + } + # Each module may have a .conf file as well, which should be + # defined in the class apache::mod::module + # Some modules do not require this file. + if defined(File["${mod}.conf"]) { + file{ "${mod}.conf symlink": + ensure => link, + path => "${enable_dir}/${mod}.conf", + target => "${mod_dir}/${mod}.conf", + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + require => [ + File["${mod}.conf"], + Exec["mkdir ${enable_dir}"], + ], + before => File[$enable_dir], + notify => Service['httpd'], + } + } + } +} diff --git a/apache/manifests/mod/alias.pp b/apache/manifests/mod/alias.pp new file mode 100644 index 000000000..ee017b490 --- /dev/null +++ b/apache/manifests/mod/alias.pp @@ -0,0 +1,19 @@ +class apache::mod::alias( + $apache_version = $apache::apache_version +) { + $icons_path = $::osfamily ? { + 'debian' => '/usr/share/apache2/icons', + 'redhat' => '/var/www/icons', + 'freebsd' => '/usr/local/www/apache22/icons', + } + apache::mod { 'alias': } + # Template uses $icons_path + file { 'alias.conf': + ensure => file, + path => "${::apache::mod_dir}/alias.conf", + content => template('apache/mod/alias.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/auth_basic.pp b/apache/manifests/mod/auth_basic.pp new file mode 100644 index 000000000..cacfafa4d --- /dev/null +++ b/apache/manifests/mod/auth_basic.pp @@ -0,0 +1,3 @@ +class apache::mod::auth_basic { + ::apache::mod { 'auth_basic': } +} diff --git a/apache/manifests/mod/auth_kerb.pp b/apache/manifests/mod/auth_kerb.pp new file mode 100644 index 000000000..6b53262a1 --- /dev/null +++ b/apache/manifests/mod/auth_kerb.pp @@ -0,0 +1,5 @@ +class apache::mod::auth_kerb { + ::apache::mod { 'auth_kerb': } +} + + diff --git a/apache/manifests/mod/authnz_ldap.pp b/apache/manifests/mod/authnz_ldap.pp new file mode 100644 index 000000000..800e656e8 --- /dev/null +++ b/apache/manifests/mod/authnz_ldap.pp @@ -0,0 +1,19 @@ +class apache::mod::authnz_ldap ( + $verifyServerCert = true, +) { + include '::apache::mod::ldap' + ::apache::mod { 'authnz_ldap': } + + validate_bool($verifyServerCert) + + # Template uses: + # - $verifyServerCert + file { 'authnz_ldap.conf': + ensure => file, + path => "${::apache::mod_dir}/authnz_ldap.conf", + content => template('apache/mod/authnz_ldap.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/autoindex.pp b/apache/manifests/mod/autoindex.pp new file mode 100644 index 000000000..f5f0f0745 --- /dev/null +++ b/apache/manifests/mod/autoindex.pp @@ -0,0 +1,12 @@ +class apache::mod::autoindex { + ::apache::mod { 'autoindex': } + # Template uses no variables + file { 'autoindex.conf': + ensure => file, + path => "${::apache::mod_dir}/autoindex.conf", + content => template('apache/mod/autoindex.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/cache.pp b/apache/manifests/mod/cache.pp new file mode 100644 index 000000000..4ab9f44ba --- /dev/null +++ b/apache/manifests/mod/cache.pp @@ -0,0 +1,3 @@ +class apache::mod::cache { + ::apache::mod { 'cache': } +} diff --git a/apache/manifests/mod/cgi.pp b/apache/manifests/mod/cgi.pp new file mode 100644 index 000000000..6c3c6aec8 --- /dev/null +++ b/apache/manifests/mod/cgi.pp @@ -0,0 +1,4 @@ +class apache::mod::cgi { + Class['::apache::mod::prefork'] -> Class['::apache::mod::cgi'] + ::apache::mod { 'cgi': } +} diff --git a/apache/manifests/mod/cgid.pp b/apache/manifests/mod/cgid.pp new file mode 100644 index 000000000..5c89251a1 --- /dev/null +++ b/apache/manifests/mod/cgid.pp @@ -0,0 +1,23 @@ +class apache::mod::cgid { + Class['::apache::mod::worker'] -> Class['::apache::mod::cgid'] + + # Debian specifies it's cgid sock path, but RedHat uses the default value + # with no config file + $cgisock_path = $::osfamily ? { + 'debian' => '${APACHE_RUN_DIR}/cgisock', + 'freebsd' => 'cgisock', + default => undef, + } + ::apache::mod { 'cgid': } + if $cgisock_path { + # Template uses $cgisock_path + file { 'cgid.conf': + ensure => file, + path => "${::apache::mod_dir}/cgid.conf", + content => template('apache/mod/cgid.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } + } +} diff --git a/apache/manifests/mod/dav.pp b/apache/manifests/mod/dav.pp new file mode 100644 index 000000000..ade9c0809 --- /dev/null +++ b/apache/manifests/mod/dav.pp @@ -0,0 +1,3 @@ +class apache::mod::dav { + ::apache::mod { 'dav': } +} diff --git a/apache/manifests/mod/dav_fs.pp b/apache/manifests/mod/dav_fs.pp new file mode 100644 index 000000000..482f31617 --- /dev/null +++ b/apache/manifests/mod/dav_fs.pp @@ -0,0 +1,20 @@ +class apache::mod::dav_fs { + $dav_lock = $::osfamily ? { + 'debian' => '${APACHE_LOCK_DIR}/DAVLock', + 'freebsd' => '/usr/local/var/DavLock', + default => '/var/lib/dav/lockdb', + } + + Class['::apache::mod::dav'] -> Class['::apache::mod::dav_fs'] + ::apache::mod { 'dav_fs': } + + # Template uses: $dav_lock + file { 'dav_fs.conf': + ensure => file, + path => "${::apache::mod_dir}/dav_fs.conf", + content => template('apache/mod/dav_fs.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/dav_svn.pp b/apache/manifests/mod/dav_svn.pp new file mode 100644 index 000000000..3ffa75911 --- /dev/null +++ b/apache/manifests/mod/dav_svn.pp @@ -0,0 +1,5 @@ +class apache::mod::dav_svn { + Class['::apache::mod::dav'] -> Class['::apache::mod::dav_svn'] + include ::apache::mod::dav + ::apache::mod { 'dav_svn': } +} diff --git a/apache/manifests/mod/deflate.pp b/apache/manifests/mod/deflate.pp new file mode 100644 index 000000000..9b597d946 --- /dev/null +++ b/apache/manifests/mod/deflate.pp @@ -0,0 +1,12 @@ +class apache::mod::deflate { + ::apache::mod { 'deflate': } + # Template uses no variables + file { 'deflate.conf': + ensure => file, + path => "${::apache::mod_dir}/deflate.conf", + content => template('apache/mod/deflate.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/dev.pp b/apache/manifests/mod/dev.pp new file mode 100644 index 000000000..5abdedd36 --- /dev/null +++ b/apache/manifests/mod/dev.pp @@ -0,0 +1,5 @@ +class apache::mod::dev { + # Development packages are not apache modules + warning('apache::mod::dev is deprecated; please use apache::dev') + include ::apache::dev +} diff --git a/apache/manifests/mod/dir.pp b/apache/manifests/mod/dir.pp new file mode 100644 index 000000000..11631305a --- /dev/null +++ b/apache/manifests/mod/dir.pp @@ -0,0 +1,21 @@ +# Note: this sets the global DirectoryIndex directive, it may be necessary to consider being able to modify the apache::vhost to declare DirectoryIndex statements in a vhost configuration +# Parameters: +# - $indexes provides a string for the DirectoryIndex directive http://httpd.apache.org/docs/current/mod/mod_dir.html#directoryindex +class apache::mod::dir ( + $dir = 'public_html', + $indexes = ['index.html','index.html.var','index.cgi','index.pl','index.php','index.xhtml'], +) { + validate_array($indexes) + ::apache::mod { 'dir': } + + # Template uses + # - $indexes + file { 'dir.conf': + ensure => file, + path => "${::apache::mod_dir}/dir.conf", + content => template('apache/mod/dir.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/disk_cache.pp b/apache/manifests/mod/disk_cache.pp new file mode 100644 index 000000000..13c9c7835 --- /dev/null +++ b/apache/manifests/mod/disk_cache.pp @@ -0,0 +1,24 @@ +class apache::mod::disk_cache { + $cache_root = $::osfamily ? { + 'debian' => '/var/cache/apache2/mod_disk_cache', + 'redhat' => '/var/cache/mod_proxy', + 'freebsd' => '/var/cache/mod_disk_cache', + } + if $::osfamily != 'FreeBSD' { + # FIXME: investigate why disk_cache was dependent on proxy + # NOTE: on FreeBSD disk_cache is compiled by default but proxy is not + Class['::apache::mod::proxy'] -> Class['::apache::mod::disk_cache'] + } + Class['::apache::mod::cache'] -> Class['::apache::mod::disk_cache'] + + apache::mod { 'disk_cache': } + # Template uses $cache_proxy + file { 'disk_cache.conf': + ensure => file, + path => "${::apache::mod_dir}/disk_cache.conf", + content => template('apache/mod/disk_cache.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/event.pp b/apache/manifests/mod/event.pp new file mode 100644 index 000000000..cad00774c --- /dev/null +++ b/apache/manifests/mod/event.pp @@ -0,0 +1,62 @@ +class apache::mod::event ( + $startservers = '2', + $maxclients = '150', + $minsparethreads = '25', + $maxsparethreads = '75', + $threadsperchild = '25', + $maxrequestsperchild = '0', + $serverlimit = '25', + $apache_version = $::apache::apache_version, +) { + if defined(Class['apache::mod::itk']) { + fail('May not include both apache::mod::event and apache::mod::itk on the same node') + } + if defined(Class['apache::mod::peruser']) { + fail('May not include both apache::mod::event and apache::mod::peruser on the same node') + } + if defined(Class['apache::mod::prefork']) { + fail('May not include both apache::mod::event and apache::mod::prefork on the same node') + } + if defined(Class['apache::mod::worker']) { + fail('May not include both apache::mod::event and apache::mod::worker on the same node') + } + File { + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + } + + # Template uses: + # - $startservers + # - $maxclients + # - $minsparethreads + # - $maxsparethreads + # - $threadsperchild + # - $maxrequestsperchild + # - $serverlimit + file { "${::apache::mod_dir}/event.conf": + ensure => file, + content => template('apache/mod/event.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } + + case $::osfamily { + 'redhat': { + if $apache_version >= 2.4 { + apache::mpm{ 'event': + apache_version => $apache_version, + } + } + } + 'debian','freebsd' : { + apache::mpm{ 'event': + apache_version => $apache_version, + } + } + default: { + fail("Unsupported osfamily ${::osfamily}") + } + } +} diff --git a/apache/manifests/mod/expires.pp b/apache/manifests/mod/expires.pp new file mode 100644 index 000000000..aae4c59d9 --- /dev/null +++ b/apache/manifests/mod/expires.pp @@ -0,0 +1,3 @@ +class apache::mod::expires { + ::apache::mod { 'expires': } +} diff --git a/apache/manifests/mod/fastcgi.pp b/apache/manifests/mod/fastcgi.pp new file mode 100644 index 000000000..a185bb31f --- /dev/null +++ b/apache/manifests/mod/fastcgi.pp @@ -0,0 +1,24 @@ +class apache::mod::fastcgi { + + # Debian specifies it's fastcgi lib path, but RedHat uses the default value + # with no config file + $fastcgi_lib_path = $::apache::params::fastcgi_lib_path + + ::apache::mod { 'fastcgi': } + + if $fastcgi_lib_path { + # Template uses: + # - $fastcgi_server + # - $fastcgi_socket + # - $fastcgi_dir + file { 'fastcgi.conf': + ensure => file, + path => "${::apache::mod_dir}/fastcgi.conf", + content => template('apache/mod/fastcgi.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } + } + +} diff --git a/apache/manifests/mod/fcgid.pp b/apache/manifests/mod/fcgid.pp new file mode 100644 index 000000000..9eb799742 --- /dev/null +++ b/apache/manifests/mod/fcgid.pp @@ -0,0 +1,3 @@ +class apache::mod::fcgid { + ::apache::mod { 'fcgid': } +} diff --git a/apache/manifests/mod/headers.pp b/apache/manifests/mod/headers.pp new file mode 100644 index 000000000..d18c5e279 --- /dev/null +++ b/apache/manifests/mod/headers.pp @@ -0,0 +1,3 @@ +class apache::mod::headers { + ::apache::mod { 'headers': } +} diff --git a/apache/manifests/mod/include.pp b/apache/manifests/mod/include.pp new file mode 100644 index 000000000..edbe81f32 --- /dev/null +++ b/apache/manifests/mod/include.pp @@ -0,0 +1,3 @@ +class apache::mod::include { + ::apache::mod { 'include': } +} diff --git a/apache/manifests/mod/info.pp b/apache/manifests/mod/info.pp new file mode 100644 index 000000000..18f9ea1df --- /dev/null +++ b/apache/manifests/mod/info.pp @@ -0,0 +1,17 @@ +class apache::mod::info ( + $allow_from = ['127.0.0.1','::1'], + $apache_version = $::apache::apache_version, +){ + apache::mod { 'info': } + # Template uses + # $allow_from + # $apache_version + file { 'info.conf': + ensure => file, + path => "${::apache::mod_dir}/info.conf", + content => template('apache/mod/info.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/itk.pp b/apache/manifests/mod/itk.pp new file mode 100644 index 000000000..1083e5ed2 --- /dev/null +++ b/apache/manifests/mod/itk.pp @@ -0,0 +1,53 @@ +class apache::mod::itk ( + $startservers = '8', + $minspareservers = '5', + $maxspareservers = '20', + $serverlimit = '256', + $maxclients = '256', + $maxrequestsperchild = '4000', + $apache_version = $::apache::apache_version, +) { + if defined(Class['apache::mod::event']) { + fail('May not include both apache::mod::itk and apache::mod::event on the same node') + } + if defined(Class['apache::mod::peruser']) { + fail('May not include both apache::mod::itk and apache::mod::peruser on the same node') + } + if defined(Class['apache::mod::prefork']) { + fail('May not include both apache::mod::itk and apache::mod::prefork on the same node') + } + if defined(Class['apache::mod::worker']) { + fail('May not include both apache::mod::itk and apache::mod::worker on the same node') + } + File { + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + } + + # Template uses: + # - $startservers + # - $minspareservers + # - $maxspareservers + # - $serverlimit + # - $maxclients + # - $maxrequestsperchild + file { "${::apache::mod_dir}/itk.conf": + ensure => file, + content => template('apache/mod/itk.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } + + case $::osfamily { + 'debian', 'freebsd': { + apache::mpm{ 'itk': + apache_version => $apache_version, + } + } + default: { + fail("Unsupported osfamily ${::osfamily}") + } + } +} diff --git a/apache/manifests/mod/ldap.pp b/apache/manifests/mod/ldap.pp new file mode 100644 index 000000000..f489291a2 --- /dev/null +++ b/apache/manifests/mod/ldap.pp @@ -0,0 +1,12 @@ +class apache::mod::ldap { + ::apache::mod { 'ldap': } + # Template uses no variables + file { 'ldap.conf': + ensure => file, + path => "${::apache::mod_dir}/ldap.conf", + content => template('apache/mod/ldap.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/mime.pp b/apache/manifests/mod/mime.pp new file mode 100644 index 000000000..8348a06ad --- /dev/null +++ b/apache/manifests/mod/mime.pp @@ -0,0 +1,21 @@ +class apache::mod::mime ( + $mime_support_package = $::apache::params::mime_support_package, + $mime_types_config = $::apache::params::mime_types_config, +) { + apache::mod { 'mime': } + # Template uses $mime_types_config + file { 'mime.conf': + ensure => file, + path => "${::apache::mod_dir}/mime.conf", + content => template('apache/mod/mime.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } + if $mime_support_package { + package { $mime_support_package: + ensure => 'installed', + before => File["${::apache::mod_dir}/mime.conf"], + } + } +} diff --git a/apache/manifests/mod/mime_magic.pp b/apache/manifests/mod/mime_magic.pp new file mode 100644 index 000000000..9de8bc4bc --- /dev/null +++ b/apache/manifests/mod/mime_magic.pp @@ -0,0 +1,14 @@ +class apache::mod::mime_magic ( + $magic_file = "${::apache::params::conf_dir}/magic" +) { + apache::mod { 'mime_magic': } + # Template uses $magic_file + file { 'mime_magic.conf': + ensure => file, + path => "${::apache::mod_dir}/mime_magic.conf", + content => template('apache/mod/mime_magic.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/negotiation.pp b/apache/manifests/mod/negotiation.pp new file mode 100644 index 000000000..eff685b15 --- /dev/null +++ b/apache/manifests/mod/negotiation.pp @@ -0,0 +1,12 @@ +class apache::mod::negotiation { + ::apache::mod { 'negotiation': } + # Template uses no variables + file { 'negotiation.conf': + ensure => file, + path => "${::apache::mod_dir}/negotiation.conf", + content => template('apache/mod/negotiation.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/nss.pp b/apache/manifests/mod/nss.pp new file mode 100644 index 000000000..f0eff1cdf --- /dev/null +++ b/apache/manifests/mod/nss.pp @@ -0,0 +1,25 @@ +class apache::mod::nss ( + $transfer_log = "${::apache::params::logroot}/access.log", + $error_log = "${::apache::params::logroot}/error.log", + $passwd_file = undef +) { + include ::apache::mod::mime + + apache::mod { 'nss': } + + $httpd_dir = $::apache::httpd_dir + + # Template uses: + # $transfer_log + # $error_log + # $http_dir + # passwd_file + file { 'nss.conf': + ensure => file, + path => "${::apache::mod_dir}/nss.conf", + content => template('apache/mod/nss.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/passenger.pp b/apache/manifests/mod/passenger.pp new file mode 100644 index 000000000..6a7404daa --- /dev/null +++ b/apache/manifests/mod/passenger.pp @@ -0,0 +1,48 @@ +class apache::mod::passenger ( + $passenger_conf_file = $::apache::params::passenger_conf_file, + $passenger_conf_package_file = $::apache::params::passenger_conf_package_file, + $passenger_high_performance = undef, + $passenger_pool_idle_time = undef, + $passenger_max_requests = undef, + $passenger_stat_throttle_rate = undef, + $rack_autodetect = undef, + $rails_autodetect = undef, + $passenger_root = $::apache::params::passenger_root, + $passenger_ruby = $::apache::params::passenger_ruby, + $passenger_max_pool_size = undef, + $passenger_use_global_queue = undef, +) { + if $::osfamily == 'FreeBSD' { + ::apache::mod { 'passenger': + lib_path => "${passenger_root}/buildout/apache2" + } + } else { + ::apache::mod { 'passenger': } + } + + # Managed by the package, but declare it to avoid purging + if $passenger_conf_package_file { + file { 'passenger_package.conf': + path => "${::apache::mod_dir}/${passenger_conf_package_file}", + } + } + + # Template uses: + # - $passenger_root + # - $passenger_ruby + # - $passenger_max_pool_size + # - $passenger_high_performance + # - $passenger_max_requests + # - $passenger_stat_throttle_rate + # - $passenger_use_global_queue + # - $rack_autodetect + # - $rails_autodetect + file { 'passenger.conf': + ensure => file, + path => "${::apache::mod_dir}/${passenger_conf_file}", + content => template('apache/mod/passenger.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/perl.pp b/apache/manifests/mod/perl.pp new file mode 100644 index 000000000..b57f25fd5 --- /dev/null +++ b/apache/manifests/mod/perl.pp @@ -0,0 +1,3 @@ +class apache::mod::perl { + ::apache::mod { 'perl': } +} diff --git a/apache/manifests/mod/peruser.pp b/apache/manifests/mod/peruser.pp new file mode 100644 index 000000000..518655a1d --- /dev/null +++ b/apache/manifests/mod/peruser.pp @@ -0,0 +1,73 @@ +class apache::mod::peruser ( + $minspareprocessors = '2', + $minprocessors = '2', + $maxprocessors = '10', + $maxclients = '150', + $maxrequestsperchild = '1000', + $idletimeout = '120', + $expiretimeout = '120', + $keepalive = 'Off', +) { + if defined(Class['apache::mod::event']) { + fail('May not include both apache::mod::peruser and apache::mod::event on the same node') + } + if defined(Class['apache::mod::itk']) { + fail('May not include both apache::mod::peruser and apache::mod::itk on the same node') + } + if defined(Class['apache::mod::prefork']) { + fail('May not include both apache::mod::peruser and apache::mod::prefork on the same node') + } + if defined(Class['apache::mod::worker']) { + fail('May not include both apache::mod::peruser and apache::mod::worker on the same node') + } + File { + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + } + + $mod_dir = $::apache::mod_dir + + # Template uses: + # - $minspareprocessors + # - $minprocessors + # - $maxprocessors + # - $maxclients + # - $maxrequestsperchild + # - $idletimeout + # - $expiretimeout + # - $keepalive + # - $mod_dir + file { "${::apache::mod_dir}/peruser.conf": + ensure => file, + content => template('apache/mod/peruser.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } + file { "${::apache::mod_dir}/peruser": + ensure => directory, + require => File[$::apache::mod_dir], + } + file { "${::apache::mod_dir}/peruser/multiplexers": + ensure => directory, + require => File["${::apache::mod_dir}/peruser"], + } + file { "${::apache::mod_dir}/peruser/processors": + ensure => directory, + require => File["${::apache::mod_dir}/peruser"], + } + + ::apache::peruser::multiplexer { '01-default': } + + case $::osfamily { + 'freebsd' : { + class { '::apache::package': + mpm_module => 'peruser' + } + } + default: { + fail("Unsupported osfamily ${::osfamily}") + } + } +} diff --git a/apache/manifests/mod/php.pp b/apache/manifests/mod/php.pp new file mode 100644 index 000000000..ace596d42 --- /dev/null +++ b/apache/manifests/mod/php.pp @@ -0,0 +1,26 @@ +class apache::mod::php ( + $package_ensure = 'present', +) { + if ! defined(Class['apache::mod::prefork']) { + fail('apache::mod::php requires apache::mod::prefork; please enable mpm_module => \'prefork\' on Class[\'apache\']') + } + ::apache::mod { 'php5': + package_ensure => $package_ensure, + } + + include ::apache::mod::mime + include ::apache::mod::dir + Class['::apache::mod::mime'] -> Class['::apache::mod::dir'] -> Class['::apache::mod::php'] + + file { 'php5.conf': + ensure => file, + path => "${::apache::mod_dir}/php5.conf", + content => template('apache/mod/php5.conf.erb'), + require => [ + Class['::apache::mod::prefork'], + Exec["mkdir ${::apache::mod_dir}"], + ], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/prefork.pp b/apache/manifests/mod/prefork.pp new file mode 100644 index 000000000..d615acbdd --- /dev/null +++ b/apache/manifests/mod/prefork.pp @@ -0,0 +1,70 @@ +class apache::mod::prefork ( + $startservers = '8', + $minspareservers = '5', + $maxspareservers = '20', + $serverlimit = '256', + $maxclients = '256', + $maxrequestsperchild = '4000', + $apache_version = $::apache::apache_version, +) { + if defined(Class['apache::mod::event']) { + fail('May not include both apache::mod::prefork and apache::mod::event on the same node') + } + if defined(Class['apache::mod::itk']) { + fail('May not include both apache::mod::prefork and apache::mod::itk on the same node') + } + if defined(Class['apache::mod::peruser']) { + fail('May not include both apache::mod::prefork and apache::mod::peruser on the same node') + } + if defined(Class['apache::mod::worker']) { + fail('May not include both apache::mod::prefork and apache::mod::worker on the same node') + } + File { + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + } + + # Template uses: + # - $startservers + # - $minspareservers + # - $maxspareservers + # - $serverlimit + # - $maxclients + # - $maxrequestsperchild + file { "${::apache::mod_dir}/prefork.conf": + ensure => file, + content => template('apache/mod/prefork.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } + + case $::osfamily { + 'redhat': { + if $apache_version >= 2.4 { + ::apache::mpm{ 'prefork': + apache_version => $apache_version, + } + } + else { + file_line { '/etc/sysconfig/httpd prefork enable': + ensure => present, + path => '/etc/sysconfig/httpd', + line => '#HTTPD=/usr/sbin/httpd.worker', + match => '#?HTTPD=/usr/sbin/httpd.worker', + require => Package['httpd'], + notify => Service['httpd'], + } + } + } + 'debian', 'freebsd' : { + ::apache::mpm{ 'prefork': + apache_version => $apache_version, + } + } + default: { + fail("Unsupported osfamily ${::osfamily}") + } + } +} diff --git a/apache/manifests/mod/proxy.pp b/apache/manifests/mod/proxy.pp new file mode 100644 index 000000000..b6c0d6df7 --- /dev/null +++ b/apache/manifests/mod/proxy.pp @@ -0,0 +1,15 @@ +class apache::mod::proxy ( + $proxy_requests = 'Off', + $allow_from = undef, +) { + ::apache::mod { 'proxy': } + # Template uses $proxy_requests + file { 'proxy.conf': + ensure => file, + path => "${::apache::mod_dir}/proxy.conf", + content => template('apache/mod/proxy.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/proxy_ajp.pp b/apache/manifests/mod/proxy_ajp.pp new file mode 100644 index 000000000..a011a1789 --- /dev/null +++ b/apache/manifests/mod/proxy_ajp.pp @@ -0,0 +1,4 @@ +class apache::mod::proxy_ajp { + Class['::apache::mod::proxy'] -> Class['::apache::mod::proxy_ajp'] + ::apache::mod { 'proxy_ajp': } +} diff --git a/apache/manifests/mod/proxy_balancer.pp b/apache/manifests/mod/proxy_balancer.pp new file mode 100644 index 000000000..5a0768d8d --- /dev/null +++ b/apache/manifests/mod/proxy_balancer.pp @@ -0,0 +1,10 @@ +class apache::mod::proxy_balancer { + + include ::apache::mod::proxy + include ::apache::mod::proxy_http + + Class['::apache::mod::proxy'] -> Class['::apache::mod::proxy_balancer'] + Class['::apache::mod::proxy_http'] -> Class['::apache::mod::proxy_balancer'] + ::apache::mod { 'proxy_balancer': } + +} diff --git a/apache/manifests/mod/proxy_html.pp b/apache/manifests/mod/proxy_html.pp new file mode 100644 index 000000000..91d7bd3c8 --- /dev/null +++ b/apache/manifests/mod/proxy_html.pp @@ -0,0 +1,28 @@ +class apache::mod::proxy_html { + Class['::apache::mod::proxy'] -> Class['::apache::mod::proxy_html'] + Class['::apache::mod::proxy_http'] -> Class['::apache::mod::proxy_html'] + ::apache::mod { 'proxy_html': } + case $::osfamily { + 'RedHat': { + ::apache::mod { 'xml2enc': } + } + 'Debian': { + $proxy_html_loadfiles = $::apache::params::distrelease ? { + '6' => '/usr/lib/libxml2.so.2', + default => "/usr/lib/${::hardwaremodel}-linux-gnu/libxml2.so.2", + } + } + 'FreeBSD': { + ::apache::mod { 'xml2enc': } + } + } + # Template uses $icons_path + file { 'proxy_html.conf': + ensure => file, + path => "${::apache::mod_dir}/proxy_html.conf", + content => template('apache/mod/proxy_html.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/proxy_http.pp b/apache/manifests/mod/proxy_http.pp new file mode 100644 index 000000000..1579e68ee --- /dev/null +++ b/apache/manifests/mod/proxy_http.pp @@ -0,0 +1,4 @@ +class apache::mod::proxy_http { + Class['::apache::mod::proxy'] -> Class['::apache::mod::proxy_http'] + ::apache::mod { 'proxy_http': } +} diff --git a/apache/manifests/mod/python.pp b/apache/manifests/mod/python.pp new file mode 100644 index 000000000..e326c8d75 --- /dev/null +++ b/apache/manifests/mod/python.pp @@ -0,0 +1,5 @@ +class apache::mod::python { + ::apache::mod { 'python': } +} + + diff --git a/apache/manifests/mod/reqtimeout.pp b/apache/manifests/mod/reqtimeout.pp new file mode 100644 index 000000000..80b301830 --- /dev/null +++ b/apache/manifests/mod/reqtimeout.pp @@ -0,0 +1,12 @@ +class apache::mod::reqtimeout { + ::apache::mod { 'reqtimeout': } + # Template uses no variables + file { 'reqtimeout.conf': + ensure => file, + path => "${::apache::mod_dir}/reqtimeout.conf", + content => template('apache/mod/reqtimeout.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/rewrite.pp b/apache/manifests/mod/rewrite.pp new file mode 100644 index 000000000..694f0b6f5 --- /dev/null +++ b/apache/manifests/mod/rewrite.pp @@ -0,0 +1,4 @@ +class apache::mod::rewrite { + include ::apache::params + ::apache::mod { 'rewrite': } +} diff --git a/apache/manifests/mod/rpaf.pp b/apache/manifests/mod/rpaf.pp new file mode 100644 index 000000000..6fbc1d4e0 --- /dev/null +++ b/apache/manifests/mod/rpaf.pp @@ -0,0 +1,20 @@ +class apache::mod::rpaf ( + $sethostname = true, + $proxy_ips = [ '127.0.0.1' ], + $header = 'X-Forwarded-For' +) { + ::apache::mod { 'rpaf': } + + # Template uses: + # - $sethostname + # - $proxy_ips + # - $header + file { 'rpaf.conf': + ensure => file, + path => "${::apache::mod_dir}/rpaf.conf", + content => template('apache/mod/rpaf.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/setenvif.pp b/apache/manifests/mod/setenvif.pp new file mode 100644 index 000000000..15b1441d8 --- /dev/null +++ b/apache/manifests/mod/setenvif.pp @@ -0,0 +1,12 @@ +class apache::mod::setenvif { + ::apache::mod { 'setenvif': } + # Template uses no variables + file { 'setenvif.conf': + ensure => file, + path => "${::apache::mod_dir}/setenvif.conf", + content => template('apache/mod/setenvif.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/ssl.pp b/apache/manifests/mod/ssl.pp new file mode 100644 index 000000000..d644ac5ef --- /dev/null +++ b/apache/manifests/mod/ssl.pp @@ -0,0 +1,55 @@ +class apache::mod::ssl ( + $ssl_compression = false, + $ssl_options = [ 'StdEnvVars' ], + $apache_version = $::apache::apache_version, +) { + $session_cache = $::osfamily ? { + 'debian' => '${APACHE_RUN_DIR}/ssl_scache(512000)', + 'redhat' => '/var/cache/mod_ssl/scache(512000)', + 'freebsd' => '/var/run/ssl_scache(512000)', + } + + case $::osfamily { + 'debian': { + if $apache_version >= 2.4 and $::operatingsystem == 'Ubuntu' { + $ssl_mutex = 'default' + } elsif $::operatingsystem == 'Ubuntu' and $::operatingsystemrelease == '10.04' { + $ssl_mutex = 'file:/var/run/apache2/ssl_mutex' + } else { + $ssl_mutex = 'file:${APACHE_RUN_DIR}/ssl_mutex' + } + } + 'redhat': { + $ssl_mutex = 'default' + } + 'freebsd': { + $ssl_mutex = 'default' + } + default: { + fail("Unsupported osfamily ${::osfamily}") + } + } + + ::apache::mod { 'ssl': } + + if $apache_version >= 2.4 { + ::apache::mod { 'socache_shmcb': } + } + + # Template uses + # + # $ssl_compression + # $ssl_options + # $session_cache, + # $ssl_mutex + # $apache_version + # + file { 'ssl.conf': + ensure => file, + path => "${::apache::mod_dir}/ssl.conf", + content => template('apache/mod/ssl.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/status.pp b/apache/manifests/mod/status.pp new file mode 100644 index 000000000..fdaba4b07 --- /dev/null +++ b/apache/manifests/mod/status.pp @@ -0,0 +1,42 @@ +# Class: apache::mod::status +# +# This class enables and configures Apache mod_status +# See: http://httpd.apache.org/docs/current/mod/mod_status.html +# +# Parameters: +# - $allow_from is an array of hosts, ip addresses, partial network numbers +# or networks in CIDR notation specifying what hosts can view the special +# /server-status URL. Defaults to ['127.0.0.1', '::1']. +# - $extended_status track and display extended status information. Valid +# values are 'On' or 'Off'. Defaults to 'On'. +# +# Actions: +# - Enable and configure Apache mod_status +# +# Requires: +# - The apache class +# +# Sample Usage: +# +# # Simple usage allowing access from localhost and a private subnet +# class { 'apache::mod::status': +# $allow_from => ['127.0.0.1', '10.10.10.10/24'], +# } +# +class apache::mod::status ( + $allow_from = ['127.0.0.1','::1'], + $extended_status = 'On', +){ + validate_array($allow_from) + validate_re(downcase($extended_status), '^(on|off)$', "${extended_status} is not supported for extended_status. Allowed values are 'On' and 'Off'.") + ::apache::mod { 'status': } + # Template uses $allow_from, $extended_status + file { 'status.conf': + ensure => file, + path => "${::apache::mod_dir}/status.conf", + content => template('apache/mod/status.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/suphp.pp b/apache/manifests/mod/suphp.pp new file mode 100644 index 000000000..f9a572f46 --- /dev/null +++ b/apache/manifests/mod/suphp.pp @@ -0,0 +1,14 @@ +class apache::mod::suphp ( +){ + ::apache::mod { 'suphp': } + + file {'suphp.conf': + ensure => file, + path => "${::apache::mod_dir}/suphp.conf", + content => template('apache/mod/suphp.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'] + } +} + diff --git a/apache/manifests/mod/userdir.pp b/apache/manifests/mod/userdir.pp new file mode 100644 index 000000000..27af54c66 --- /dev/null +++ b/apache/manifests/mod/userdir.pp @@ -0,0 +1,17 @@ +class apache::mod::userdir ( + $home = '/home', + $dir = 'public_html', + $disable_root = true, +) { + ::apache::mod { 'userdir': } + + # Template uses $home, $dir, $disable_root + file { 'userdir.conf': + ensure => file, + path => "${::apache::mod_dir}/userdir.conf", + content => template('apache/mod/userdir.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/mod/vhost_alias.pp b/apache/manifests/mod/vhost_alias.pp new file mode 100644 index 000000000..30ae122e1 --- /dev/null +++ b/apache/manifests/mod/vhost_alias.pp @@ -0,0 +1,3 @@ +class apache::mod::vhost_alias { + ::apache::mod { 'vhost_alias': } +} diff --git a/apache/manifests/mod/worker.pp b/apache/manifests/mod/worker.pp new file mode 100644 index 000000000..8007953cf --- /dev/null +++ b/apache/manifests/mod/worker.pp @@ -0,0 +1,72 @@ +class apache::mod::worker ( + $startservers = '2', + $maxclients = '150', + $minsparethreads = '25', + $maxsparethreads = '75', + $threadsperchild = '25', + $maxrequestsperchild = '0', + $serverlimit = '25', + $apache_version = $::apache::apache_version, +) { + if defined(Class['apache::mod::event']) { + fail('May not include both apache::mod::worker and apache::mod::event on the same node') + } + if defined(Class['apache::mod::itk']) { + fail('May not include both apache::mod::worker and apache::mod::itk on the same node') + } + if defined(Class['apache::mod::peruser']) { + fail('May not include both apache::mod::worker and apache::mod::peruser on the same node') + } + if defined(Class['apache::mod::prefork']) { + fail('May not include both apache::mod::worker and apache::mod::prefork on the same node') + } + File { + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + } + + # Template uses: + # - $startservers + # - $maxclients + # - $minsparethreads + # - $maxsparethreads + # - $threadsperchild + # - $maxrequestsperchild + # - $serverlimit + file { "${::apache::mod_dir}/worker.conf": + ensure => file, + content => template('apache/mod/worker.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'], + } + + case $::osfamily { + 'redhat': { + if $apache_version >= 2.4 { + ::apache::mpm{ 'worker': + apache_version => $apache_version, + } + } + else { + file_line { '/etc/sysconfig/httpd worker enable': + ensure => present, + path => '/etc/sysconfig/httpd', + line => 'HTTPD=/usr/sbin/httpd.worker', + match => '#?HTTPD=/usr/sbin/httpd.worker', + require => Package['httpd'], + notify => Service['httpd'], + } + } + } + 'debian', 'freebsd': { + ::apache::mpm{ 'worker': + apache_version => $apache_version, + } + } + default: { + fail("Unsupported osfamily ${::osfamily}") + } + } +} diff --git a/apache/manifests/mod/wsgi.pp b/apache/manifests/mod/wsgi.pp new file mode 100644 index 000000000..244a3458b --- /dev/null +++ b/apache/manifests/mod/wsgi.pp @@ -0,0 +1,21 @@ +class apache::mod::wsgi ( + $wsgi_socket_prefix = undef, + $wsgi_python_path = undef, + $wsgi_python_home = undef, +){ + ::apache::mod { 'wsgi': } + + # Template uses: + # - $wsgi_socket_prefix + # - $wsgi_python_path + # - $wsgi_python_home + file {'wsgi.conf': + ensure => file, + path => "${::apache::mod_dir}/wsgi.conf", + content => template('apache/mod/wsgi.conf.erb'), + require => Exec["mkdir ${::apache::mod_dir}"], + before => File[$::apache::mod_dir], + notify => Service['httpd'] + } +} + diff --git a/apache/manifests/mod/xsendfile.pp b/apache/manifests/mod/xsendfile.pp new file mode 100644 index 000000000..7c5e88437 --- /dev/null +++ b/apache/manifests/mod/xsendfile.pp @@ -0,0 +1,4 @@ +class apache::mod::xsendfile { + include ::apache::params + ::apache::mod { 'xsendfile': } +} diff --git a/apache/manifests/mpm.pp b/apache/manifests/mpm.pp new file mode 100644 index 000000000..b6b2cfebe --- /dev/null +++ b/apache/manifests/mpm.pp @@ -0,0 +1,68 @@ +define apache::mpm ( + $lib_path = $::apache::params::lib_path, + $apache_version = $::apache::apache_version, +) { + if ! defined(Class['apache']) { + fail('You must include the apache base class before using any apache defined resources') + } + + $mpm = $name + $mod_dir = $::apache::mod_dir + + $_lib = "mod_mpm_${mpm}.so" + $_path = "${lib_path}/${_lib}" + $_id = "mpm_${mpm}_module" + + if $apache_version >= 2.4 { + file { "${mod_dir}/${mpm}.load": + ensure => file, + path => "${mod_dir}/${mpm}.load", + content => "LoadModule ${_id} ${_path}\n", + require => [ + Package['httpd'], + Exec["mkdir ${mod_dir}"], + ], + before => File[$mod_dir], + notify => Service['httpd'], + } + } + + case $::osfamily { + 'debian': { + file { "${::apache::mod_enable_dir}/${mpm}.conf": + ensure => link, + target => "${::apache::mod_dir}/${mpm}.conf", + require => Exec["mkdir ${::apache::mod_enable_dir}"], + before => File[$::apache::mod_enable_dir], + notify => Service['httpd'], + } + + if $apache_version >= 2.4 { + file { "${::apache::mod_enable_dir}/${mpm}.load": + ensure => link, + target => "${::apache::mod_dir}/${mpm}.load", + require => Exec["mkdir ${::apache::mod_enable_dir}"], + before => File[$::apache::mod_enable_dir], + notify => Service['httpd'], + } + } + + if $apache_version < 2.4 { + package { "apache2-mpm-${mpm}": + ensure => present, + } + } + } + 'freebsd': { + class { '::apache::package': + mpm_module => $mpm + } + } + 'redhat': { + # so we don't fail + } + default: { + fail("Unsupported osfamily ${::osfamily}") + } + } +} diff --git a/apache/manifests/namevirtualhost.pp b/apache/manifests/namevirtualhost.pp new file mode 100644 index 000000000..4fa879518 --- /dev/null +++ b/apache/manifests/namevirtualhost.pp @@ -0,0 +1,9 @@ +define apache::namevirtualhost { + $addr_port = $name + + # Template uses: $addr_port + concat::fragment { "NameVirtualHost ${addr_port}": + target => $::apache::ports_file, + content => template('apache/namevirtualhost.erb'), + } +} diff --git a/apache/manifests/package.pp b/apache/manifests/package.pp new file mode 100644 index 000000000..c5ef31536 --- /dev/null +++ b/apache/manifests/package.pp @@ -0,0 +1,48 @@ +class apache::package ( + $ensure = 'present', + $mpm_module = $::apache::params::mpm_module, +) { + case $::osfamily { + 'freebsd' : { + $all_mpms = [ + 'www/apache22', + 'www/apache22-worker-mpm', + 'www/apache22-event-mpm', + 'www/apache22-itk-mpm', + 'www/apache22-peruser-mpm', + ] + if $mpm_module { + $apache_package = $mpm_module ? { + 'prefork' => 'www/apache22', + default => "www/apache22-${mpm_module}-mpm" + } + } else { + $apache_package = 'www/apache22' + } + $other_mpms = delete($all_mpms, $apache_package) + # Configure ports to have apache module packages dependent on correct + # version of apache package (apache22, apache22-worker-mpm, ...) + file_line { 'APACHE_PORT in /etc/make.conf': + ensure => $ensure, + path => '/etc/make.conf', + line => "APACHE_PORT=${apache_package}", + match => '^\\s*#?\\s*APACHE_PORT\\s*=\\s*', + before => Package['httpd'], + } + # remove other packages + ensure_resource('package', $other_mpms, { + ensure => absent, + before => Package['httpd'], + require => File_line['APACHE_PORT in /etc/make.conf'], + }) + } + default: { + $apache_package = $::apache::params::apache_name + } + } + package { 'httpd': + ensure => $ensure, + name => $apache_package, + notify => Class['Apache::Service'], + } +} diff --git a/apache/manifests/params.pp b/apache/manifests/params.pp new file mode 100644 index 000000000..04c3b65fe --- /dev/null +++ b/apache/manifests/params.pp @@ -0,0 +1,206 @@ +# Class: apache::params +# +# This class manages Apache parameters +# +# Parameters: +# - The $user that Apache runs as +# - The $group that Apache runs as +# - The $apache_name is the name of the package and service on the relevant +# distribution +# - The $php_package is the name of the package that provided PHP +# - The $ssl_package is the name of the Apache SSL package +# - The $apache_dev is the name of the Apache development libraries package +# - The $conf_contents is the contents of the Apache configuration file +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +class apache::params inherits ::apache::version { + if($::fqdn) { + $servername = $::fqdn + } else { + $servername = $::hostname + } + + # The default error log level + $log_level = 'warn' + + if $::osfamily == 'RedHat' or $::operatingsystem == 'amazon' { + $user = 'apache' + $group = 'apache' + $root_group = 'root' + $apache_name = 'httpd' + $service_name = 'httpd' + $httpd_dir = '/etc/httpd' + $server_root = '/etc/httpd' + $conf_dir = "${httpd_dir}/conf" + $confd_dir = "${httpd_dir}/conf.d" + $mod_dir = "${httpd_dir}/conf.d" + $vhost_dir = "${httpd_dir}/conf.d" + $conf_file = 'httpd.conf' + $ports_file = "${conf_dir}/ports.conf" + $logroot = '/var/log/httpd' + $lib_path = 'modules' + $mpm_module = 'prefork' + $dev_packages = 'httpd-devel' + $default_ssl_cert = '/etc/pki/tls/certs/localhost.crt' + $default_ssl_key = '/etc/pki/tls/private/localhost.key' + $ssl_certs_dir = '/etc/pki/tls/certs' + $passenger_conf_file = 'passenger_extra.conf' + $passenger_conf_package_file = 'passenger.conf' + $passenger_root = undef + $passenger_ruby = undef + $suphp_addhandler = 'php5-script' + $suphp_engine = 'off' + $suphp_configpath = undef + $mod_packages = { + 'auth_kerb' => 'mod_auth_kerb', + 'authnz_ldap' => 'mod_authz_ldap', + 'fastcgi' => 'mod_fastcgi', + 'fcgid' => 'mod_fcgid', + 'passenger' => 'mod_passenger', + 'perl' => 'mod_perl', + 'php5' => $distrelease ? { + '5' => 'php53', + default => 'php', + }, + 'proxy_html' => 'mod_proxy_html', + 'python' => 'mod_python', + 'shibboleth' => 'shibboleth', + 'ssl' => 'mod_ssl', + 'wsgi' => 'mod_wsgi', + 'dav_svn' => 'mod_dav_svn', + 'suphp' => 'mod_suphp', + 'xsendfile' => 'mod_xsendfile', + 'nss' => 'mod_nss', + } + $mod_libs = { + 'php5' => 'libphp5.so', + 'nss' => 'libmodnss.so', + } + $conf_template = 'apache/httpd.conf.erb' + $keepalive = 'Off' + $keepalive_timeout = 15 + $fastcgi_lib_path = undef + $mime_support_package = 'mailcap' + $mime_types_config = '/etc/mime.types' + } elsif $::osfamily == 'Debian' { + $user = 'www-data' + $group = 'www-data' + $root_group = 'root' + $apache_name = 'apache2' + $service_name = 'apache2' + $httpd_dir = '/etc/apache2' + $server_root = '/etc/apache2' + $conf_dir = $httpd_dir + $confd_dir = "${httpd_dir}/conf.d" + $mod_dir = "${httpd_dir}/mods-available" + $mod_enable_dir = "${httpd_dir}/mods-enabled" + $vhost_dir = "${httpd_dir}/sites-available" + $vhost_enable_dir = "${httpd_dir}/sites-enabled" + $conf_file = 'apache2.conf' + $ports_file = "${conf_dir}/ports.conf" + $logroot = '/var/log/apache2' + $lib_path = '/usr/lib/apache2/modules' + $mpm_module = 'worker' + $dev_packages = ['libaprutil1-dev', 'libapr1-dev', 'apache2-prefork-dev'] + $default_ssl_cert = '/etc/ssl/certs/ssl-cert-snakeoil.pem' + $default_ssl_key = '/etc/ssl/private/ssl-cert-snakeoil.key' + $ssl_certs_dir = '/etc/ssl/certs' + $passenger_conf_file = 'passenger.conf' + $passenger_conf_package_file = undef + $passenger_root = '/usr' + $passenger_ruby = '/usr/bin/ruby' + $suphp_addhandler = 'x-httpd-php' + $suphp_engine = 'off' + $suphp_configpath = '/etc/php5/apache2' + $mod_packages = { + 'auth_kerb' => 'libapache2-mod-auth-kerb', + 'dav_svn' => 'libapache2-svn', + 'fastcgi' => 'libapache2-mod-fastcgi', + 'fcgid' => 'libapache2-mod-fcgid', + 'nss' => 'libapache2-mod-nss', + 'passenger' => 'libapache2-mod-passenger', + 'perl' => 'libapache2-mod-perl2', + 'php5' => 'libapache2-mod-php5', + 'proxy_html' => 'libapache2-mod-proxy-html', + 'python' => 'libapache2-mod-python', + 'rpaf' => 'libapache2-mod-rpaf', + 'suphp' => 'libapache2-mod-suphp', + 'wsgi' => 'libapache2-mod-wsgi', + 'xsendfile' => 'libapache2-mod-xsendfile', + } + $mod_libs = { + 'php5' => 'libphp5.so', + } + $conf_template = 'apache/httpd.conf.erb' + $keepalive = 'Off' + $keepalive_timeout = 15 + $fastcgi_lib_path = '/var/lib/apache2/fastcgi' + $mime_support_package = 'mime-support' + $mime_types_config = '/etc/mime.types' + } elsif $::osfamily == 'FreeBSD' { + $user = 'www' + $group = 'www' + $root_group = 'wheel' + $apache_name = 'apache22' + $service_name = 'apache22' + $httpd_dir = '/usr/local/etc/apache22' + $server_root = '/usr/local' + $conf_dir = $httpd_dir + $confd_dir = "${httpd_dir}/Includes" + $mod_dir = "${httpd_dir}/Modules" + $mod_enable_dir = undef + $vhost_dir = "${httpd_dir}/Vhosts" + $vhost_enable_dir = undef + $conf_file = 'httpd.conf' + $ports_file = "${conf_dir}/ports.conf" + $logroot = '/var/log/apache22' + $lib_path = '/usr/local/libexec/apache22' + $mpm_module = 'prefork' + $dev_packages = undef + $default_ssl_cert = '/usr/local/etc/apache22/server.crt' + $default_ssl_key = '/usr/local/etc/apache22/server.key' + $ssl_certs_dir = '/usr/local/etc/apache22' + $passenger_conf_file = 'passenger.conf' + $passenger_conf_package_file = undef + $passenger_root = '/usr/local/lib/ruby/gems/1.9/gems/passenger-4.0.10' + $passenger_ruby = '/usr/bin/ruby' + $suphp_addhandler = 'php5-script' + $suphp_engine = 'off' + $suphp_configpath = undef + $mod_packages = { + # NOTE: I list here only modules that are not included in www/apache22 + # NOTE: 'passenger' needs to enable APACHE_SUPPORT in make config + # NOTE: 'php' needs to enable APACHE option in make config + # NOTE: 'dav_svn' needs to enable MOD_DAV_SVN make config + # NOTE: not sure where the shibboleth should come from + # NOTE: don't know where the shibboleth module should come from + 'auth_kerb' => 'www/mod_auth_kerb2', + 'fcgid' => 'www/mod_fcgid', + 'passenger' => 'www/rubygem-passenger', + 'perl' => 'www/mod_perl2', + 'php5' => 'lang/php5', + 'proxy_html' => 'www/mod_proxy_html', + 'python' => 'www/mod_python3', + 'wsgi' => 'www/mod_wsgi', + 'dav_svn' => 'devel/subversion', + 'xsendfile' => 'www/mod_xsendfile', + 'rpaf' => 'www/mod_rpaf2' + } + $mod_libs = { + 'php5' => 'libphp5.so', + } + $conf_template = 'apache/httpd.conf.erb' + $keepalive = 'Off' + $keepalive_timeout = 15 + $fastcgi_lib_path = undef # TODO: revisit + $mime_support_package = 'misc/mime-support' + $mime_types_config = '/usr/local/etc/mime.types' + } else { + fail("Class['apache::params']: Unsupported osfamily: ${::osfamily}") + } +} diff --git a/apache/manifests/peruser/multiplexer.pp b/apache/manifests/peruser/multiplexer.pp new file mode 100644 index 000000000..9e57ac30b --- /dev/null +++ b/apache/manifests/peruser/multiplexer.pp @@ -0,0 +1,17 @@ +define apache::peruser::multiplexer ( + $user = $::apache::user, + $group = $::apache::group, + $file = undef, +) { + if ! $file { + $filename = "${name}.conf" + } else { + $filename = $file + } + file { "${::apache::mod_dir}/peruser/multiplexers/${filename}": + ensure => file, + content => "Multiplexer ${user} ${group}\n", + require => File["${::apache::mod_dir}/peruser/multiplexers"], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/peruser/processor.pp b/apache/manifests/peruser/processor.pp new file mode 100644 index 000000000..1d6893465 --- /dev/null +++ b/apache/manifests/peruser/processor.pp @@ -0,0 +1,17 @@ +define apache::peruser::processor ( + $user, + $group, + $file = undef, +) { + if ! $file { + $filename = "${name}.conf" + } else { + $filename = $file + } + file { "${::apache::mod_dir}/peruser/processors/${filename}": + ensure => file, + content => "Processor ${user} ${group}\n", + require => File["${::apache::mod_dir}/peruser/processors"], + notify => Service['httpd'], + } +} diff --git a/apache/manifests/php.pp b/apache/manifests/php.pp new file mode 100644 index 000000000..9fa9c682e --- /dev/null +++ b/apache/manifests/php.pp @@ -0,0 +1,18 @@ +# Class: apache::php +# +# This class installs PHP for Apache +# +# Parameters: +# - $php_package +# +# Actions: +# - Install Apache PHP package +# +# Requires: +# +# Sample Usage: +# +class apache::php { + warning('apache::php is deprecated; please use apache::mod::php') + include ::apache::mod::php +} diff --git a/apache/manifests/proxy.pp b/apache/manifests/proxy.pp new file mode 100644 index 000000000..050f36c27 --- /dev/null +++ b/apache/manifests/proxy.pp @@ -0,0 +1,15 @@ +# Class: apache::proxy +# +# This class enabled the proxy module for Apache +# +# Actions: +# - Enables Apache Proxy module +# +# Requires: +# +# Sample Usage: +# +class apache::proxy { + warning('apache::proxy is deprecated; please use apache::mod::proxy') + include ::apache::mod::proxy +} diff --git a/apache/manifests/python.pp b/apache/manifests/python.pp new file mode 100644 index 000000000..723a753f8 --- /dev/null +++ b/apache/manifests/python.pp @@ -0,0 +1,18 @@ +# Class: apache::python +# +# This class installs Python for Apache +# +# Parameters: +# - $php_package +# +# Actions: +# - Install Apache Python package +# +# Requires: +# +# Sample Usage: +# +class apache::python { + warning('apache::python is deprecated; please use apache::mod::python') + include ::apache::mod::python +} diff --git a/apache/manifests/service.pp b/apache/manifests/service.pp new file mode 100644 index 000000000..b21a25f4b --- /dev/null +++ b/apache/manifests/service.pp @@ -0,0 +1,35 @@ +# Class: apache::service +# +# Manages the Apache daemon +# +# Parameters: +# +# Actions: +# - Manage Apache service +# +# Requires: +# +# Sample Usage: +# +# sometype { 'foo': +# notify => Class['apache::service], +# } +# +# +class apache::service ( + $service_name = $::apache::params::service_name, + $service_enable = true, + $service_ensure = 'running', +) { + # The base class must be included first because parameter defaults depend on it + if ! defined(Class['apache::params']) { + fail('You must include the apache::params class before using any apache defined resources') + } + validate_bool($service_enable) + + service { 'httpd': + ensure => $service_ensure, + name => $service_name, + enable => $service_enable, + } +} diff --git a/apache/manifests/ssl.pp b/apache/manifests/ssl.pp new file mode 100644 index 000000000..d0b36593d --- /dev/null +++ b/apache/manifests/ssl.pp @@ -0,0 +1,18 @@ +# Class: apache::ssl +# +# This class installs Apache SSL capabilities +# +# Parameters: +# - The $ssl_package name from the apache::params class +# +# Actions: +# - Install Apache SSL capabilities +# +# Requires: +# +# Sample Usage: +# +class apache::ssl { + warning('apache::ssl is deprecated; please use apache::mod::ssl') + include ::apache::mod::ssl +} diff --git a/apache/manifests/version.pp b/apache/manifests/version.pp new file mode 100644 index 000000000..581fdde6f --- /dev/null +++ b/apache/manifests/version.pp @@ -0,0 +1,35 @@ +# Class: apache::version +# +# Try to automatically detect the version by OS +# +class apache::version { + # This will be 5 or 6 on RedHat, 6 or wheezy on Debian, 12 or quantal on Ubuntu, 3 on Amazon, etc. + $osr_array = split($::operatingsystemrelease,'[\/\.]') + $distrelease = $osr_array[0] + if ! $distrelease { + fail("Class['apache::params']: Unparsable \$::operatingsystemrelease: ${::operatingsystemrelease}") + } + + case $::osfamily { + 'RedHat': { + if ($::operatingsystem == 'Fedora' and $distrelease >= 18) or ($::operatingsystem != 'Fedora' and $distrelease >= 7) { + $default = 2.4 + } else { + $default = 2.2 + } + } + 'Debian': { + if $::operatingsystem == 'Ubuntu' and $distrelease >= 13.10 { + $default = 2.4 + } else { + $default = 2.2 + } + } + 'FreeBSD': { + $default = 2.2 + } + default: { + fail("Class['apache::version']: Unsupported osfamily: ${::osfamily}") + } + } +} diff --git a/apache/manifests/vhost.pp b/apache/manifests/vhost.pp new file mode 100644 index 000000000..8525235e9 --- /dev/null +++ b/apache/manifests/vhost.pp @@ -0,0 +1,548 @@ +# Definition: apache::vhost +# +# This class installs Apache Virtual Hosts +# +# Parameters: +# - The $port to configure the host on +# - The $docroot provides the DocumentRoot variable +# - The $virtual_docroot provides VirtualDocumentationRoot variable +# - The $serveradmin will specify an email address for Apache that it will +# display when it renders one of it's error pages +# - The $ssl option is set true or false to enable SSL for this Virtual Host +# - The $priority of the site +# - The $servername is the primary name of the virtual host +# - The $serveraliases of the site +# - The $ip to configure the host on, defaulting to * +# - The $options for the given vhost +# - The $override for the given vhost (list of AllowOverride arguments) +# - The $vhost_name for name based virtualhosting, defaulting to * +# - The $logroot specifies the location of the virtual hosts logfiles, default +# to /var/log// +# - The $log_level specifies the verbosity of the error log for this vhost. Not +# set by default for the vhost, instead the global server configuration default +# of 'warn' is used. +# - The $access_log specifies if *_access.log directives should be configured. +# - The $ensure specifies if vhost file is present or absent. +# - The $headers is a list of Header statement strings as per http://httpd.apache.org/docs/2.2/mod/mod_headers.html#header +# - The $request_headers is a list of RequestHeader statement strings as per http://httpd.apache.org/docs/2.2/mod/mod_headers.html#requestheader +# - $aliases is a list of Alias hashes for mod_alias as per http://httpd.apache.org/docs/current/mod/mod_alias.html +# each statement is a hash in the form of { alias => '/alias', path => '/real/path/to/directory' } +# - $directories is a lost of hashes for creating statements as per http://httpd.apache.org/docs/2.2/mod/core.html#directory +# each statement is a hash in the form of { path => '/path/to/directory', => } +# see README.md for list of supported directives. +# +# Actions: +# - Install Apache Virtual Hosts +# +# Requires: +# - The apache class +# +# Sample Usage: +# +# # Simple vhost definition: +# apache::vhost { 'site.name.fqdn': +# port => '80', +# docroot => '/path/to/docroot', +# } +# +# # Multiple Mod Rewrites: +# apache::vhost { 'site.name.fqdn': +# port => '80', +# docroot => '/path/to/docroot', +# rewrites => [ +# { +# comment => 'force www domain', +# rewrite_cond => ['%{HTTP_HOST} ^([a-z.]+)?example.com$ [NC]', '%{HTTP_HOST} !^www. [NC]'], +# rewrite_rule => ['.? http://www.%1example.com%{REQUEST_URI} [R=301,L]'] +# }, +# { +# comment => 'prevent image hotlinking', +# rewrite_cond => ['%{HTTP_REFERER} !^$', '%{HTTP_REFERER} !^http://(www.)?example.com/ [NC]'], +# rewrite_rule => ['.(gif|jpg|png)$ - [F]'] +# }, +# ] +# } +# +# # SSL vhost with non-SSL rewrite: +# apache::vhost { 'site.name.fqdn': +# port => '443', +# ssl => true, +# docroot => '/path/to/docroot', +# } +# apache::vhost { 'site.name.fqdn': +# port => '80', +# rewrites => [ +# { +# comment => "redirect non-SSL traffic to SSL site", +# rewrite_cond => ['%{HTTPS} off'], +# rewrite_rule => ['(.*) https://%{HTTPS_HOST}%{REQUEST_URI}'] +# } +# ] +# } +# apache::vhost { 'site.name.fqdn': +# port => '80', +# docroot => '/path/to/other_docroot', +# custom_fragment => template("${module_name}/my_fragment.erb"), +# } +# +define apache::vhost( + $docroot, + $virtual_docroot = false, + $port = undef, + $ip = undef, + $ip_based = false, + $add_listen = true, + $docroot_owner = 'root', + $docroot_group = $::apache::params::root_group, + $serveradmin = undef, + $ssl = false, + $ssl_cert = $::apache::default_ssl_cert, + $ssl_key = $::apache::default_ssl_key, + $ssl_chain = $::apache::default_ssl_chain, + $ssl_ca = $::apache::default_ssl_ca, + $ssl_crl_path = $::apache::default_ssl_crl_path, + $ssl_crl = $::apache::default_ssl_crl, + $ssl_certs_dir = $::apache::params::ssl_certs_dir, + $ssl_protocol = undef, + $ssl_cipher = undef, + $ssl_honorcipherorder = undef, + $ssl_verify_client = undef, + $ssl_verify_depth = undef, + $ssl_options = undef, + $ssl_proxyengine = false, + $priority = undef, + $default_vhost = false, + $servername = $name, + $serveraliases = [], + $options = ['Indexes','FollowSymLinks','MultiViews'], + $override = ['None'], + $directoryindex = '', + $vhost_name = '*', + $logroot = $::apache::logroot, + $log_level = undef, + $access_log = true, + $access_log_file = undef, + $access_log_pipe = undef, + $access_log_syslog = undef, + $access_log_format = undef, + $access_log_env_var = undef, + $aliases = undef, + $directories = undef, + $error_log = true, + $error_log_file = undef, + $error_log_pipe = undef, + $error_log_syslog = undef, + $error_documents = [], + $fallbackresource = undef, + $scriptalias = undef, + $scriptaliases = [], + $proxy_dest = undef, + $proxy_pass = undef, + $suphp_addhandler = $::apache::params::suphp_addhandler, + $suphp_engine = $::apache::params::suphp_engine, + $suphp_configpath = $::apache::params::suphp_configpath, + $php_admin_flags = [], + $php_admin_values = [], + $no_proxy_uris = [], + $redirect_source = '/', + $redirect_dest = undef, + $redirect_status = undef, + $redirectmatch_status = undef, + $redirectmatch_regexp = undef, + $rack_base_uris = undef, + $headers = undef, + $request_headers = undef, + $rewrites = undef, + $rewrite_base = undef, + $rewrite_rule = undef, + $rewrite_cond = undef, + $setenv = [], + $setenvif = [], + $block = [], + $ensure = 'present', + $wsgi_application_group = undef, + $wsgi_daemon_process = undef, + $wsgi_daemon_process_options = undef, + $wsgi_import_script = undef, + $wsgi_import_script_options = undef, + $wsgi_process_group = undef, + $wsgi_script_aliases = undef, + $custom_fragment = undef, + $itk = undef, + $fastcgi_server = undef, + $fastcgi_socket = undef, + $fastcgi_dir = undef, + $additional_includes = [], + $apache_version = $::apache::apache_version + ) { + # The base class must be included first because it is used by parameter defaults + if ! defined(Class['apache']) { + fail('You must include the apache base class before using any apache defined resources') + } + + $apache_name = $::apache::params::apache_name + + validate_re($ensure, '^(present|absent)$', + "${ensure} is not supported for ensure. + Allowed values are 'present' and 'absent'.") + validate_re($suphp_engine, '^(on|off)$', + "${suphp_engine} is not supported for suphp_engine. + Allowed values are 'on' and 'off'.") + validate_bool($ip_based) + validate_bool($access_log) + validate_bool($error_log) + validate_bool($ssl) + validate_bool($default_vhost) + validate_bool($ssl_proxyengine) + if $rewrites { + validate_array($rewrites) + validate_hash($rewrites[0]) + } + + # Deprecated backwards-compatibility + if $rewrite_base { + warning('Apache::Vhost: parameter rewrite_base is deprecated in favor of rewrites') + } + if $rewrite_rule { + warning('Apache::Vhost: parameter rewrite_rule is deprecated in favor of rewrites') + } + if $rewrite_cond { + warning('Apache::Vhost parameter rewrite_cond is deprecated in favor of rewrites') + } + + if $wsgi_script_aliases { + validate_hash($wsgi_script_aliases) + } + if $wsgi_daemon_process_options { + validate_hash($wsgi_daemon_process_options) + } + if $wsgi_import_script_options { + validate_hash($wsgi_import_script_options) + } + if $itk { + validate_hash($itk) + } + + if $log_level { + validate_re($log_level, '^(emerg|alert|crit|error|warn|notice|info|debug)$', + "Log level '${log_level}' is not one of the supported Apache HTTP Server log levels.") + } + + if $access_log_file and $access_log_pipe { + fail("Apache::Vhost[${name}]: 'access_log_file' and 'access_log_pipe' cannot be defined at the same time") + } + + if $error_log_file and $error_log_pipe { + fail("Apache::Vhost[${name}]: 'error_log_file' and 'error_log_pipe' cannot be defined at the same time") + } + + if $fallbackresource { + validate_re($fallbackresource, '^/|disabled', 'Please make sure fallbackresource starts with a / (or is "disabled")') + } + + if $ssl and $ensure == 'present' { + include ::apache::mod::ssl + # Required for the AddType lines. + include ::apache::mod::mime + } + + if $virtual_docroot { + include ::apache::mod::vhost_alias + } + + # This ensures that the docroot exists + # But enables it to be specified across multiple vhost resources + if ! defined(File[$docroot]) { + file { $docroot: + ensure => directory, + owner => $docroot_owner, + group => $docroot_group, + require => Package['httpd'], + } + } + + # Same as above, but for logroot + if ! defined(File[$logroot]) { + file { $logroot: + ensure => directory, + require => Package['httpd'], + } + } + + + # Is apache::mod::passenger enabled (or apache::mod['passenger']) + $passenger_enabled = defined(Apache::Mod['passenger']) + + # Define log file names + if $access_log_file { + $access_log_destination = "${logroot}/${access_log_file}" + } elsif $access_log_pipe { + $access_log_destination = $access_log_pipe + } elsif $access_log_syslog { + $access_log_destination = $access_log_syslog + } else { + if $ssl { + $access_log_destination = "${logroot}/${name}_access_ssl.log" + } else { + $access_log_destination = "${logroot}/${name}_access.log" + } + } + + if $error_log_file { + $error_log_destination = "${logroot}/${error_log_file}" + } elsif $error_log_pipe { + $error_log_destination = $error_log_pipe + } elsif $error_log_syslog { + $error_log_destination = $error_log_syslog + } else { + if $ssl { + $error_log_destination = "${logroot}/${name}_error_ssl.log" + } else { + $error_log_destination = "${logroot}/${name}_error.log" + } + } + + # Set access log format + if $access_log_format { + $_access_log_format = "\"${access_log_format}\"" + } else { + $_access_log_format = 'combined' + } + + if $access_log_env_var { + $_access_log_env_var = "env=${access_log_env_var}" + } + + if $ip { + if $port { + $listen_addr_port = "${ip}:${port}" + $nvh_addr_port = "${ip}:${port}" + } else { + $nvh_addr_port = $ip + if ! $servername and ! $ip_based { + fail("Apache::Vhost[${name}]: must pass 'ip' and/or 'port' parameters for name-based vhosts") + } + } + } else { + if $port { + $listen_addr_port = $port + $nvh_addr_port = "${vhost_name}:${port}" + } else { + $nvh_addr_port = $name + if ! $servername { + fail("Apache::Vhost[${name}]: must pass 'ip' and/or 'port' parameters, and/or 'servername' parameter") + } + } + } + if $add_listen { + if $ip and defined(Apache::Listen[$port]) { + fail("Apache::Vhost[${name}]: Mixing IP and non-IP Listen directives is not possible; check the add_listen parameter of the apache::vhost define to disable this") + } + if ! defined(Apache::Listen[$listen_addr_port]) and $listen_addr_port and $ensure == 'present' { + ::apache::listen { $listen_addr_port: } + } + } + if ! $ip_based { + if ! defined(Apache::Namevirtualhost[$nvh_addr_port]) and $ensure == 'present' { + ::apache::namevirtualhost { $nvh_addr_port: } + } + } + + # Load mod_rewrite if needed and not yet loaded + if $rewrites or $rewrite_cond { + if ! defined(Apache::Mod['rewrite']) { + ::apache::mod { 'rewrite': } + } + } + + # Load mod_alias if needed and not yet loaded + if ($scriptalias or $scriptaliases != []) or ($redirect_source and $redirect_dest) { + if ! defined(Class['apache::mod::alias']) { + include ::apache::mod::alias + } + } + + # Load mod_proxy if needed and not yet loaded + if ($proxy_dest or $proxy_pass) { + if ! defined(Class['apache::mod::proxy']) { + include ::apache::mod::proxy + } + if ! defined(Class['apache::mod::proxy_http']) { + include ::apache::mod::proxy_http + } + } + + # Load mod_passenger if needed and not yet loaded + if $rack_base_uris { + if ! defined(Class['apache::mod::passenger']) { + include ::apache::mod::passenger + } + } + + # Load mod_fastci if needed and not yet loaded + if $fastcgi_server and $fastcgi_socket { + if ! defined(Class['apache::mod::fastcgi']) { + include ::apache::mod::fastcgi + } + } + + # Configure the defaultness of a vhost + if $priority { + $priority_real = $priority + } elsif $default_vhost { + $priority_real = '10' + } else { + $priority_real = '25' + } + + # Check if mod_headers is required to process $headers/$request_headers + if $headers or $request_headers { + if ! defined(Class['apache::mod::headers']) { + include ::apache::mod::headers + } + } + + ## Apache include does not always work with spaces in the filename + $filename = regsubst($name, ' ', '_', 'G') + + ## Create a default directory list if none defined + if $directories { + if !is_hash($directories) and !(is_array($directories) and is_hash($directories[0])) { + fail("Apache::Vhost[${name}]: 'directories' must be either a Hash or an Array of Hashes") + } + $_directories = $directories + } else { + $_directory = { + provider => 'directory', + path => $docroot, + options => $options, + allow_override => $override, + directoryindex => $directoryindex, + } + + if $apache_version == 2.4 { + $_directory_version = { + require => 'all granted', + } + } else { + $_directory_version = { + order => 'allow,deny', + allow => 'from all', + } + } + + $_directories = [ merge($_directory, $_directory_version) ] + } + + # Template uses: + # - $nvh_addr_port + # - $servername + # - $serveradmin + # - $docroot + # - $virtual_docroot + # - $options + # - $override + # - $logroot + # - $name + # - $aliases + # - $_directories + # - $log_level + # - $access_log + # - $access_log_destination + # - $_access_log_format + # - $_access_log_env_var + # - $error_log + # - $error_log_destination + # - $error_documents + # - $fallbackresource + # - $custom_fragment + # - $additional_includes + # block fragment: + # - $block + # directories fragment: + # - $passenger_enabled + # - $php_admin_flags + # - $php_admin_values + # - $directories (a list of key-value hashes is expected) + # fastcgi fragment: + # - $fastcgi_server + # - $fastcgi_socket + # - $fastcgi_dir + # proxy fragment: + # - $proxy_dest + # - $no_proxy_uris + # rack fragment: + # - $rack_base_uris + # redirect fragment: + # - $redirect_source + # - $redirect_dest + # - $redirect_status + # header fragment + # - $headers + # requestheader fragment: + # - $request_headers + # rewrite fragment: + # - $rewrites + # scriptalias fragment: + # - $scriptalias + # - $scriptaliases + # - $ssl + # serveralias fragment: + # - $serveraliases + # setenv fragment: + # - $setenv + # - $setenvif + # ssl fragment: + # - $ssl + # - $ssl_cert + # - $ssl_key + # - $ssl_chain + # - $ssl_certs_dir + # - $ssl_ca + # - $ssl_crl + # - $ssl_crl_path + # - $ssl_verify_client + # - $ssl_verify_depth + # - $ssl_options + # suphp fragment: + # - $suphp_addhandler + # - $suphp_engine + # - $suphp_configpath + # wsgi fragment: + # - $wsgi_application_group + # - $wsgi_daemon_process + # - $wsgi_import_script + # - $wsgi_process_group + # - $wsgi_script_aliases + file { "${priority_real}-${filename}.conf": + ensure => $ensure, + path => "${::apache::vhost_dir}/${priority_real}-${filename}.conf", + content => template('apache/vhost.conf.erb'), + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + require => [ + Package['httpd'], + File[$docroot], + File[$logroot], + ], + notify => Service['httpd'], + } + if $::osfamily == 'Debian' { + $vhost_enable_dir = $::apache::vhost_enable_dir + $vhost_symlink_ensure = $ensure ? { + present => link, + default => $ensure, + } + file{ "${priority_real}-${filename}.conf symlink": + ensure => $vhost_symlink_ensure, + path => "${vhost_enable_dir}/${priority_real}-${filename}.conf", + target => "${::apache::vhost_dir}/${priority_real}-${filename}.conf", + owner => 'root', + group => $::apache::params::root_group, + mode => '0644', + require => File["${priority_real}-${filename}.conf"], + notify => Service['httpd'], + } + } +} diff --git a/apache/spec/acceptance/apache_parameters_spec.rb b/apache/spec/acceptance/apache_parameters_spec.rb new file mode 100644 index 000000000..eeae55d45 --- /dev/null +++ b/apache/spec/acceptance/apache_parameters_spec.rb @@ -0,0 +1,399 @@ +require 'spec_helper_acceptance' +require_relative './version.rb' + +describe 'apache parameters', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + + # Currently this test only does something on FreeBSD. + describe 'default_confd_files => false' do + it 'doesnt do anything' do + pp = "class { 'apache': default_confd_files => false }" + apply_manifest(pp, :catch_failures => true) + end + + if fact('osfamily') == 'FreeBSD' + describe file("#{confd_dir}/no-accf.conf.erb") do + it { should_not be_file } + end + end + end + describe 'default_confd_files => true' do + it 'copies conf.d files' do + pp = "class { 'apache': default_confd_files => true }" + apply_manifest(pp, :catch_failures => true) + end + + if fact('osfamily') == 'FreeBSD' + describe file("#{$confd_dir}/no-accf.conf.erb") do + it { should be_file } + end + end + end + + describe 'when set adds a listen statement' do + it 'applys cleanly' do + pp = "class { 'apache': ip => '10.1.1.1', service_ensure => stopped }" + apply_manifest(pp, :catch_failures => true) + end + + describe file($ports_file) do + it { should be_file } + it { should contain 'Listen 10.1.1.1' } + end + end + + describe 'service tests => true' do + it 'starts the service' do + pp = <<-EOS + class { 'apache': + service_enable => true, + service_ensure => running, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service($service_name) do + it { should be_running } + it { should be_enabled } + end + end + + describe 'service tests => false' do + it 'stops the service' do + pp = <<-EOS + class { 'apache': + service_enable => false, + service_ensure => stopped, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service($service_name) do + it { should_not be_running } + it { should_not be_enabled } + end + end + + describe 'purge parameters => false' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': + purge_configs => false, + purge_vdir => false, + } + EOS + shell("touch #{$confd_dir}/test.conf") + apply_manifest(pp, :catch_failures => true) + end + + # Ensure the file didn't disappear. + describe file("#{$confd_dir}/test.conf") do + it { should be_file } + end + end + + if fact('osfamily') != 'Debian' + describe 'purge parameters => true' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': + purge_configs => true, + purge_vdir => true, + } + EOS + shell("touch #{$confd_dir}/test.conf") + apply_manifest(pp, :catch_failures => true) + end + + # File should be gone + describe file("#{$confd_dir}/test.conf") do + it { should_not be_file } + end + end + end + + describe 'serveradmin' do + it 'applies cleanly' do + pp = "class { 'apache': serveradmin => 'test@example.com' }" + apply_manifest(pp, :catch_failures => true) + end + + describe file($vhost) do + it { should be_file } + it { should contain 'ServerAdmin test@example.com' } + end + end + + describe 'sendfile' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': sendfile => 'On' }" + apply_manifest(pp, :catch_failures => true) + end + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'EnableSendfile On' } + end + + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': sendfile => 'Off' }" + apply_manifest(pp, :catch_failures => true) + end + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'Sendfile Off' } + end + end + + describe 'error_documents' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': error_documents => true }" + apply_manifest(pp, :catch_failures => true) + end + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'Alias /error/' } + end + end + + describe 'timeout' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': timeout => '1234' }" + apply_manifest(pp, :catch_failures => true) + end + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'Timeout 1234' } + end + end + + describe 'httpd_dir' do + describe 'setup' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': httpd_dir => '/tmp', service_ensure => stopped } + include 'apache::mod::mime' + EOS + apply_manifest(pp, :catch_failures => true) + end + end + + describe file("#{$confd_dir}/mime.conf") do + it { should be_file } + it { should contain 'AddLanguage eo .eo' } + end + end + + describe 'server_root' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': server_root => '/tmp/root', service_ensure => stopped }" + apply_manifest(pp, :catch_failures => true) + end + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'ServerRoot "/tmp/root"' } + end + end + + describe 'confd_dir' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': confd_dir => '/tmp/root', service_ensure => stopped }" + apply_manifest(pp, :catch_failures => true) + end + end + + if $apache_version >= 2.4 + describe file($conf_file) do + it { should be_file } + it { should contain 'IncludeOptional "/tmp/root/*.conf"' } + end + else + describe file($conf_file) do + it { should be_file } + it { should contain 'Include "/tmp/root/*.conf"' } + end + end + end + + describe 'conf_template' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': conf_template => 'another/test.conf.erb', service_ensure => stopped }" + shell("mkdir -p #{default['distmoduledir']}/another/templates") + shell("echo 'testcontent' >> #{default['distmoduledir']}/another/templates/test.conf.erb") + apply_manifest(pp, :catch_failures => true) + end + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'testcontent' } + end + end + + describe 'servername' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': servername => 'test.server', service_ensure => stopped }" + apply_manifest(pp, :catch_failures => true) + end + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'ServerName "test.server"' } + end + end + + describe 'user' do + describe 'setup' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': + manage_user => true, + manage_group => true, + user => 'testweb', + group => 'testweb', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + end + + describe user('testweb') do + it { should exist } + it { should belong_to_group 'testweb' } + end + + describe group('testweb') do + it { should exist } + end + end + + describe 'keepalive' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': keepalive => 'On', keepalive_timeout => '30' }" + apply_manifest(pp, :catch_failures => true) + end + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'KeepAlive On' } + it { should contain 'KeepAliveTimeout 30' } + end + end + + describe 'logging' do + describe 'setup' do + it 'applies cleanly' do + pp = "class { 'apache': logroot => '/tmp' }" + apply_manifest(pp, :catch_failures => true) + end + end + + describe file("/tmp/#{$error_log}") do + it { should be_file } + end + end + + describe 'ports_file' do + it 'applys cleanly' do + pp = <<-EOS + class { 'apache': + ports_file => '/tmp/ports_file', + ip => '10.1.1.1', + service_ensure => stopped + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file('/tmp/ports_file') do + it { should be_file } + it { should contain 'Listen 10.1.1.1' } + end + end + + describe 'server_tokens' do + it 'applys cleanly' do + pp = <<-EOS + class { 'apache': + server_tokens => 'Minor', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'ServerTokens Minor' } + end + end + + describe 'server_signature' do + it 'applys cleanly' do + pp = <<-EOS + class { 'apache': + server_signature => 'testsig', + service_ensure => stopped, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'ServerSignature testsig' } + end + end + + describe 'trace_enable' do + it 'applys cleanly' do + pp = <<-EOS + class { 'apache': + trace_enable => 'Off', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file($conf_file) do + it { should be_file } + it { should contain 'TraceEnable Off' } + end + end + + describe 'package_ensure' do + it 'applys cleanly' do + pp = <<-EOS + class { 'apache': + package_ensure => present, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe package($package_name) do + it { should be_installed } + end + end + +end diff --git a/apache/spec/acceptance/apache_ssl_spec.rb b/apache/spec/acceptance/apache_ssl_spec.rb new file mode 100644 index 000000000..649c02d84 --- /dev/null +++ b/apache/spec/acceptance/apache_ssl_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper_acceptance' + +case fact('osfamily') +when 'RedHat' + vhostd = '/etc/httpd/conf.d' +when 'Debian' + vhostd = '/etc/apache2/sites-available' +end + +describe 'apache ssl', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + + describe 'ssl parameters' do + it 'runs without error' do + pp = <<-EOS + class { 'apache': + service_ensure => stopped, + default_ssl_vhost => true, + default_ssl_cert => '/tmp/ssl_cert', + default_ssl_key => '/tmp/ssl_key', + default_ssl_chain => '/tmp/ssl_chain', + default_ssl_ca => '/tmp/ssl_ca', + default_ssl_crl_path => '/tmp/ssl_crl_path', + default_ssl_crl => '/tmp/ssl_crl', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{vhostd}/15-default-ssl.conf") do + it { should be_file } + it { should contain 'SSLCertificateFile "/tmp/ssl_cert"' } + it { should contain 'SSLCertificateKeyFile "/tmp/ssl_key"' } + it { should contain 'SSLCertificateChainFile "/tmp/ssl_chain"' } + it { should contain 'SSLCACertificateFile "/tmp/ssl_ca"' } + it { should contain 'SSLCARevocationPath "/tmp/ssl_crl_path"' } + it { should contain 'SSLCARevocationFile "/tmp/ssl_crl"' } + end + end + + describe 'vhost ssl parameters' do + it 'runs without error' do + pp = <<-EOS + class { 'apache': + service_ensure => stopped, + } + + apache::vhost { 'test_ssl': + docroot => '/tmp/test', + ssl => true, + ssl_cert => '/tmp/ssl_cert', + ssl_key => '/tmp/ssl_key', + ssl_chain => '/tmp/ssl_chain', + ssl_ca => '/tmp/ssl_ca', + ssl_crl_path => '/tmp/ssl_crl_path', + ssl_crl => '/tmp/ssl_crl', + ssl_certs_dir => '/tmp', + ssl_protocol => 'test', + ssl_cipher => 'test', + ssl_honorcipherorder => 'test', + ssl_verify_client => 'test', + ssl_verify_depth => 'test', + ssl_options => ['test', 'test1'], + ssl_proxyengine => true, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{vhostd}/25-test_ssl.conf") do + it { should be_file } + it { should contain 'SSLCertificateFile "/tmp/ssl_cert"' } + it { should contain 'SSLCertificateKeyFile "/tmp/ssl_key"' } + it { should contain 'SSLCertificateChainFile "/tmp/ssl_chain"' } + it { should contain 'SSLCACertificateFile "/tmp/ssl_ca"' } + it { should contain 'SSLCARevocationPath "/tmp/ssl_crl_path"' } + it { should contain 'SSLCARevocationFile "/tmp/ssl_crl"' } + it { should contain 'SSLProxyEngine On' } + it { should contain 'SSLProtocol test' } + it { should contain 'SSLCipherSuite test' } + it { should contain 'SSLHonorCipherOrder test' } + it { should contain 'SSLVerifyClient test' } + it { should contain 'SSLVerifyDepth test' } + it { should contain 'SSLOptions test test1' } + end + end + +end diff --git a/apache/spec/acceptance/basic_spec.rb b/apache/spec/acceptance/basic_spec.rb new file mode 100644 index 000000000..6c2b3f462 --- /dev/null +++ b/apache/spec/acceptance/basic_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper_acceptance' + +describe 'disable selinux:', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + it "because otherwise apache won't work" do + apply_manifest(%{ + exec { "setenforce 0": + path => "/bin:/sbin:/usr/bin:/usr/sbin", + onlyif => "which setenforce && getenforce | grep Enforcing", + } + }, :catch_failures => true) + end +end diff --git a/apache/spec/acceptance/class_spec.rb b/apache/spec/acceptance/class_spec.rb new file mode 100644 index 000000000..1f5921d59 --- /dev/null +++ b/apache/spec/acceptance/class_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper_acceptance' + +describe 'apache class', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + case fact('osfamily') + when 'RedHat' + package_name = 'httpd' + service_name = 'httpd' + when 'Debian' + package_name = 'apache2' + service_name = 'apache2' + when 'FreeBSD' + package_name = 'apache22' + service_name = 'apache22' + end + + context 'default parameters' do + it 'should work with no errors' do + pp = <<-EOS + class { 'apache': } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + + describe package(package_name) do + it { should be_installed } + end + + describe service(service_name) do + it { should be_enabled } + it { should be_running } + end + end + + context 'custom site/mod dir parameters' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + file { '/tmp/apache_custom': ensure => directory, } + class { 'apache': + mod_dir => '/tmp/apache_custom/mods', + vhost_dir => '/tmp/apache_custom/vhosts', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + describe service(service_name) do + it { should be_enabled } + it { should be_running } + end + end +end diff --git a/apache/spec/acceptance/default_mods_spec.rb b/apache/spec/acceptance/default_mods_spec.rb new file mode 100644 index 000000000..03e144560 --- /dev/null +++ b/apache/spec/acceptance/default_mods_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper_acceptance' + +case fact('osfamily') +when 'RedHat' + servicename = 'httpd' +when 'Debian' + servicename = 'apache2' +when 'FreeBSD' + servicename = 'apache22' +else + raise "Unconfigured OS for apache service on #{fact('osfamily')}" +end + +describe 'apache::default_mods class', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + describe 'no default mods' do + # Using puppet_apply as a helper + it 'should apply with no errors' do + pp = <<-EOS + class { 'apache': + default_mods => false, + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + + describe service(servicename) do + it { should be_running } + end + end + + describe 'no default mods and failing' do + # Using puppet_apply as a helper + it 'should apply with errors' do + pp = <<-EOS + class { 'apache': + default_mods => false, + } + apache::vhost { 'defaults.example.com': + docroot => '/var/www/defaults', + aliases => { + alias => '/css', + path => '/var/www/css', + }, + setenv => 'TEST1 one', + } + EOS + + apply_manifest(pp, { :expect_failures => true }) + end + + # Are these the same? + describe service(servicename) do + it { should_not be_running } + end + describe "service #{servicename}" do + it 'should not be running' do + shell("pidof #{servicename}", {:acceptable_exit_codes => 1}) + end + end + end + + describe 'alternative default mods' do + # Using puppet_apply as a helper + it 'should apply with no errors' do + pp = <<-EOS + class { 'apache': + default_mods => [ + 'info', + 'alias', + 'mime', + 'env', + 'expires', + ], + } + apache::vhost { 'defaults.example.com': + docroot => '/var/www/defaults', + aliases => { + alias => '/css', + path => '/var/www/css', + }, + setenv => 'TEST1 one', + } + EOS + + apply_manifest(pp, :catch_failures => true) + shell('sleep 10') + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + + describe service(servicename) do + it { should be_running } + end + end +end diff --git a/apache/spec/acceptance/itk_spec.rb b/apache/spec/acceptance/itk_spec.rb new file mode 100644 index 000000000..86fc2c01c --- /dev/null +++ b/apache/spec/acceptance/itk_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper_acceptance' + +case fact('osfamily') +when 'Debian' + service_name = 'apache2' +when 'FreeBSD' + service_name = 'apache22' +else + # Not implemented yet + service_name = :skip +end + +describe 'apache::mod::itk class', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) or service_name.equal? :skip do + describe 'running puppet code' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'apache': + mpm_module => 'itk', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + end + + describe service(service_name) do + it { should be_running } + it { should be_enabled } + end +end diff --git a/apache/spec/acceptance/mod_php_spec.rb b/apache/spec/acceptance/mod_php_spec.rb new file mode 100644 index 000000000..d1c991621 --- /dev/null +++ b/apache/spec/acceptance/mod_php_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper_acceptance' + +describe 'apache::mod::php class', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + case fact('osfamily') + when 'Debian' + vhost_dir = '/etc/apache2/sites-enabled' + mod_dir = '/etc/apache2/mods-available' + service_name = 'apache2' + when 'RedHat' + vhost_dir = '/etc/httpd/conf.d' + mod_dir = '/etc/httpd/conf.d' + service_name = 'httpd' + when 'FreeBSD' + vhost_dir = '/usr/local/etc/apache22/Vhosts' + mod_dir = '/usr/local/etc/apache22/Modules' + service_name = 'apache22' + end + + context "default php config" do + it 'succeeds in puppeting php' do + pp= <<-EOS + class { 'apache': + mpm_module => 'prefork', + } + class { 'apache::mod::php': } + apache::vhost { 'php.example.com': + port => '80', + docroot => '/var/www/php', + } + host { 'php.example.com': ip => '127.0.0.1', } + file { '/var/www/php/index.php': + ensure => file, + content => "\\n", + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service(service_name) do + it { should be_enabled } + it { should be_running } + end + + describe file("#{mod_dir}/php5.conf") do + it { should contain "DirectoryIndex index.php" } + end + + it 'should answer to php.example.com' do + shell("/usr/bin/curl php.example.com:80") do |r| + r.stdout.should =~ /PHP Version/ + r.exit_code.should == 0 + end + end + end + + context "custom php admin_flag and php_admin_value" do + it 'succeeds in puppeting php' do + pp= <<-EOS + class { 'apache': + mpm_module => 'prefork', + } + class { 'apache::mod::php': } + apache::vhost { 'php.example.com': + port => '80', + docroot => '/var/www/php', + php_admin_values => { 'open_basedir' => '/var/www/php/:/usr/share/pear/', }, + php_admin_flags => { 'engine' => 'on', }, + } + host { 'php.example.com': ip => '127.0.0.1', } + file { '/var/www/php/index.php': + ensure => file, + content => "\\n", + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service(service_name) do + it { should be_enabled } + it { should be_running } + end + + describe file("#{vhost_dir}/25-php.example.com.conf") do + it { should contain " php_admin_flag engine on" } + it { should contain " php_admin_value open_basedir /var/www/php/:/usr/share/pear/" } + end + + it 'should answer to php.example.com' do + shell("/usr/bin/curl php.example.com:80") do |r| + r.stdout.should =~ /\/usr\/share\/pear\// + r.exit_code.should == 0 + end + end + end +end diff --git a/apache/spec/acceptance/mod_suphp_spec.rb b/apache/spec/acceptance/mod_suphp_spec.rb new file mode 100644 index 000000000..9e26731d6 --- /dev/null +++ b/apache/spec/acceptance/mod_suphp_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper_acceptance' + +describe 'apache::mod::suphp class', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + case fact('osfamily') + when 'Debian' + context "default suphp config" do + it 'succeeds in puppeting suphp' do + pp = <<-EOS + class { 'apache': + mpm_module => 'prefork', + } + class { 'apache::mod::php': } + class { 'apache::mod::suphp': } + apache::vhost { 'suphp.example.com': + port => '80', + docroot => '/var/www/suphp', + } + host { 'suphp.example.com': ip => '127.0.0.1', } + file { '/var/www/suphp/index.php': + ensure => file, + owner => 'daemon', + group => 'daemon', + content => "\\n", + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service('apache2') do + it { should be_enabled } + it { should be_running } + end + + it 'should answer to suphp.example.com' do + shell("/usr/bin/curl suphp.example.com:80") do |r| + r.stdout.should =~ /^daemon$/ + r.exit_code.should == 0 + end + end + end + when 'RedHat' + # Not implemented yet + end +end diff --git a/apache/spec/acceptance/nodesets/centos-59-x64.yml b/apache/spec/acceptance/nodesets/centos-59-x64.yml new file mode 100644 index 000000000..cde1fe5a8 --- /dev/null +++ b/apache/spec/acceptance/nodesets/centos-59-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + centos-59-x64: + roles: + - master + platform: el-5-x86_64 + box : centos-59-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-59-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git diff --git a/apache/spec/acceptance/nodesets/centos-64-x64-pe.yml b/apache/spec/acceptance/nodesets/centos-64-x64-pe.yml new file mode 100644 index 000000000..e408d1be7 --- /dev/null +++ b/apache/spec/acceptance/nodesets/centos-64-x64-pe.yml @@ -0,0 +1,13 @@ +HOSTS: + centos-64-x64: + roles: + - master + - database + - dashboard + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: pe diff --git a/apache/spec/acceptance/nodesets/centos-64-x64.yml b/apache/spec/acceptance/nodesets/centos-64-x64.yml new file mode 100644 index 000000000..ce47212a8 --- /dev/null +++ b/apache/spec/acceptance/nodesets/centos-64-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + centos-64-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git diff --git a/apache/spec/acceptance/nodesets/debian-607-x64.yml b/apache/spec/acceptance/nodesets/debian-607-x64.yml new file mode 100644 index 000000000..e642e0992 --- /dev/null +++ b/apache/spec/acceptance/nodesets/debian-607-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + debian-607-x64: + roles: + - master + platform: debian-6-amd64 + box : debian-607-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-607-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git diff --git a/apache/spec/acceptance/nodesets/debian-70rc1-x64.yml b/apache/spec/acceptance/nodesets/debian-70rc1-x64.yml new file mode 100644 index 000000000..cbbbfb2cc --- /dev/null +++ b/apache/spec/acceptance/nodesets/debian-70rc1-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + debian-70rc1-x64: + roles: + - master + platform: debian-7-amd64 + box : debian-70rc1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-70rc1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git diff --git a/apache/spec/acceptance/nodesets/default.yml b/apache/spec/acceptance/nodesets/default.yml new file mode 120000 index 000000000..2719644a6 --- /dev/null +++ b/apache/spec/acceptance/nodesets/default.yml @@ -0,0 +1 @@ +centos-64-x64.yml \ No newline at end of file diff --git a/apache/spec/acceptance/nodesets/fedora-18-x64.yml b/apache/spec/acceptance/nodesets/fedora-18-x64.yml new file mode 100644 index 000000000..086cae995 --- /dev/null +++ b/apache/spec/acceptance/nodesets/fedora-18-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + fedora-18-x64: + roles: + - master + platform: fedora-18-x86_64 + box : fedora-18-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/fedora-18-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git diff --git a/apache/spec/acceptance/nodesets/sles-11sp1-x64.yml b/apache/spec/acceptance/nodesets/sles-11sp1-x64.yml new file mode 100644 index 000000000..a9f01d5f4 --- /dev/null +++ b/apache/spec/acceptance/nodesets/sles-11sp1-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + sles-11sp1-x64: + roles: + - master + platform: sles-11-x86_64 + box : sles-11sp1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/sles-11sp1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git diff --git a/apache/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml b/apache/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml new file mode 100644 index 000000000..c1b8bdf8f --- /dev/null +++ b/apache/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + ubuntu-server-10044-x64: + roles: + - master + platform: ubuntu-10.04-amd64 + box : ubuntu-server-10044-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-10044-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git diff --git a/apache/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml b/apache/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml new file mode 100644 index 000000000..f7df2ccce --- /dev/null +++ b/apache/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + ubuntu-server-12042-x64: + roles: + - master + platform: ubuntu-12.04-amd64 + box : ubuntu-server-12042-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git diff --git a/apache/spec/acceptance/nodesets/ubuntu-server-1310-x64.yml b/apache/spec/acceptance/nodesets/ubuntu-server-1310-x64.yml new file mode 100644 index 000000000..f4b2366f3 --- /dev/null +++ b/apache/spec/acceptance/nodesets/ubuntu-server-1310-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + ubuntu-server-1310-x64: + roles: + - master + platform: ubuntu-13.10-amd64 + box : ubuntu-server-1310-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-1310-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + log_level : debug + type: git diff --git a/apache/spec/acceptance/prefork_worker_spec.rb b/apache/spec/acceptance/prefork_worker_spec.rb new file mode 100644 index 000000000..beffe0a01 --- /dev/null +++ b/apache/spec/acceptance/prefork_worker_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper_acceptance' + +case fact('osfamily') +when 'RedHat' + servicename = 'httpd' +when 'Debian' + servicename = 'apache2' +when 'FreeBSD' + servicename = 'apache22' +end + +case fact('osfamily') +when 'FreeBSD' + describe 'apache::mod::event class' do + describe 'running puppet code' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'apache': + mpm_module => 'event', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + end + + describe service(servicename) do + it { should be_running } + it { should be_enabled } + end + end +end + +describe 'apache::mod::worker class', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + describe 'running puppet code' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'apache': + mpm_module => 'worker', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + end + + describe service(servicename) do + it { should be_running } + it { should be_enabled } + end +end + +describe 'apache::mod::prefork class', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + describe 'running puppet code' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'apache': + mpm_module => 'prefork', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + end + + describe service(servicename) do + it { should be_running } + it { should be_enabled } + end +end diff --git a/apache/spec/acceptance/service_spec.rb b/apache/spec/acceptance/service_spec.rb new file mode 100644 index 000000000..b51ca386f --- /dev/null +++ b/apache/spec/acceptance/service_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper_acceptance' + +describe 'apache::service class', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + describe 'adding dependencies in between the base class and service class' do + it 'should work with no errors' do + pp = <<-EOS + class { 'apache': } + file { '/tmp/test': + require => Class['apache'], + notify => Class['apache::service'], + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + end +end diff --git a/apache/spec/acceptance/unsupported_spec.rb b/apache/spec/acceptance/unsupported_spec.rb new file mode 100644 index 000000000..085845dbf --- /dev/null +++ b/apache/spec/acceptance/unsupported_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper_acceptance' + +describe 'unsupported distributions and OSes', :if => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + it 'should fail' do + pp = <<-EOS + class { 'apache': } + apache::vhost { 'test.lan': + docroot => '/var/www', + } + EOS + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/unsupported/i) + end +end diff --git a/apache/spec/acceptance/version.rb b/apache/spec/acceptance/version.rb new file mode 100644 index 000000000..169054ec2 --- /dev/null +++ b/apache/spec/acceptance/version.rb @@ -0,0 +1,55 @@ +_osfamily = fact('osfamily') +_operatingsystem = fact('operatingsystem') +_operatingsystemrelease = fact('operatingsystemrelease').to_f + +case _osfamily +when 'RedHat' + $confd_dir = '/etc/httpd/conf.d' + $conf_file = '/etc/httpd/conf/httpd.conf' + $ports_file = '/etc/httpd/conf/ports.conf' + $vhost_dir = '/etc/httpd/conf.d' + $vhost = '/etc/httpd/conf.d/15-default.conf' + $run_dir = '/var/run/httpd' + $service_name = 'httpd' + $package_name = 'httpd' + $error_log = 'error_log' + $suphp_handler = 'php5-script' + $suphp_configpath = 'undef' + + if (_operatingsystem == 'Fedora' and _operatingsystemrelease >= 18) or (_operatingsystem != 'Fedora' and _operatingsystemrelease >= 7) + $apache_version = 2.4 + else + $apache_version = 2.2 + end +when 'Debian' + $confd_dir = '/etc/apache2/mods-available' + $conf_file = '/etc/apache2/apache2.conf' + $ports_file = '/etc/apache2/ports.conf' + $vhost = '/etc/apache2/sites-available/15-default.conf' + $vhost_dir = '/etc/apache2/sites-enabled' + $run_dir = '/var/run/apache2' + $service_name = 'apache2' + $package_name = 'apache2' + $error_log = 'error.log' + $suphp_handler = 'x-httpd-php' + $suphp_configpath = '/etc/php5/apache2' + + if _operatingsystem == 'Ubuntu' and _operatingsystemrelease >= 13.10 + $apache_version = 2.4 + else + $apache_version = 2.2 + end +when 'FreeBSD' + $confd_dir = '/usr/local/etc/apache22/Includes' + $conf_file = '/usr/local/etc/apache22/httpd.conf' + $ports_file = '/usr/local/etc/apache22/Includes/ports.conf' + $vhost = '/usr/local/etc/apache22/Vhosts/15-default.conf' + $vhost_dir = '/usr/local/etc/apache22/Vhosts' + $run_dir = '/var/run/apache22' + $service_name = 'apache22' + $package_name = 'apache22' + $error_log = 'http-error.log' + + $apache_version = 2.2 +end + diff --git a/apache/spec/acceptance/vhost_spec.rb b/apache/spec/acceptance/vhost_spec.rb new file mode 100644 index 000000000..f6749d35a --- /dev/null +++ b/apache/spec/acceptance/vhost_spec.rb @@ -0,0 +1,996 @@ +require 'spec_helper_acceptance' +require_relative './version.rb' + +describe 'apache::vhost define', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + context 'no default vhosts' do + it 'should create no default vhosts' do + pp = <<-EOS + class { 'apache': + default_vhost => false, + default_ssl_vhost => false, + service_ensure => stopped + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/15-default.conf") do + it { should_not be_file } + end + + describe file("#{$vhost_dir}/15-default-ssl.conf") do + it { should_not be_file } + end + end + + context "default vhost without ssl" do + it 'should create a default vhost config' do + pp = <<-EOS + class { 'apache': } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/15-default.conf") do + it { should contain '' } + end + + describe file("#{$vhost_dir}/15-default-ssl.conf") do + it { should_not be_file } + end + end + + context 'default vhost with ssl' do + it 'should create default vhost configs' do + pp = <<-EOS + file { '#{$run_dir}': + ensure => 'directory', + recurse => true, + } + + class { 'apache': + default_ssl_vhost => true, + require => File['#{$run_dir}'], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/15-default.conf") do + it { should contain '' } + end + + describe file("#{$vhost_dir}/15-default-ssl.conf") do + it { should contain '' } + it { should contain "SSLEngine on" } + end + end + + context 'new vhost on port 80' do + it 'should configure an apache vhost' do + pp = <<-EOS + class { 'apache': } + file { '#{$run_dir}': + ensure => 'directory', + recurse => true, + } + + apache::vhost { 'first.example.com': + port => '80', + docroot => '/var/www/first', + require => File['#{$run_dir}'], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-first.example.com.conf") do + it { should contain '' } + it { should contain "ServerName first.example.com" } + end + end + + context 'new proxy vhost on port 80' do + it 'should configure an apache proxy vhost' do + pp = <<-EOS + class { 'apache': } + apache::vhost { 'proxy.example.com': + port => '80', + docroot => '/var/www/proxy', + proxy_pass => [ + { 'path' => '/foo', 'url' => 'http://backend-foo/'}, + ], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-proxy.example.com.conf") do + it { should contain '' } + it { should contain "ServerName proxy.example.com" } + it { should contain "ProxyPass" } + it { should_not contain "" } + end + end + + context 'new vhost on port 80' do + it 'should configure two apache vhosts' do + pp = <<-EOS + class { 'apache': } + apache::vhost { 'first.example.com': + port => '80', + docroot => '/var/www/first', + } + host { 'first.example.com': ip => '127.0.0.1', } + file { '/var/www/first/index.html': + ensure => file, + content => "Hello from first\\n", + } + apache::vhost { 'second.example.com': + port => '80', + docroot => '/var/www/second', + } + host { 'second.example.com': ip => '127.0.0.1', } + file { '/var/www/second/index.html': + ensure => file, + content => "Hello from second\\n", + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service($service_name) do + it { should be_enabled } + it { should be_running } + end + + it 'should answer to first.example.com' do + shell("/usr/bin/curl first.example.com:80", {:acceptable_exit_codes => 0}) do |r| + r.stdout.should == "Hello from first\n" + end + end + + it 'should answer to second.example.com' do + shell("/usr/bin/curl second.example.com:80", {:acceptable_exit_codes => 0}) do |r| + r.stdout.should == "Hello from second\n" + end + end + end + + context 'apache_directories' do + describe 'readme example, adapted' do + it 'should configure a vhost with Files' do + pp = <<-EOS + class { 'apache': } + + if $apache::apache_version >= 2.4 { + $_files_match_directory = { 'path' => '(\.swp|\.bak|~)$', 'provider' => 'filesmatch', 'require' => 'all denied', } + } else { + $_files_match_directory = { 'path' => '(\.swp|\.bak|~)$', 'provider' => 'filesmatch', 'deny' => 'from all', } + } + + $_directories = [ + { 'path' => '/var/www/files', }, + $_files_match_directory, + ] + + apache::vhost { 'files.example.net': + docroot => '/var/www/files', + directories => $_directories, + } + file { '/var/www/files/index.html': + ensure => file, + content => "Hello World\\n", + } + file { '/var/www/files/index.html.bak': + ensure => file, + content => "Hello World\\n", + } + host { 'files.example.net': ip => '127.0.0.1', } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service($service_name) do + it { should be_enabled } + it { should be_running } + end + + it 'should answer to files.example.net' do + shell("/usr/bin/curl -sSf files.example.net:80/index.html").stdout.should eq("Hello World\n") + shell("/usr/bin/curl -sSf files.example.net:80/index.html.bak", {:acceptable_exit_codes => 22}).stderr.should match(/curl: \(22\) The requested URL returned error: 403/) + end + end + + describe 'other Directory options' do + it 'should configure a vhost with multiple Directory sections' do + pp = <<-EOS + class { 'apache': } + + if $apache::apache_version >= 2.4 { + $_files_match_directory = { 'path' => 'private.html$', 'provider' => 'filesmatch', 'require' => 'all denied' } + } else { + $_files_match_directory = { 'path' => 'private.html$', 'provider' => 'filesmatch', 'deny' => 'from all' } + } + + $_directories = [ + { 'path' => '/var/www/files', }, + { 'path' => '/foo/', 'provider' => 'location', 'directoryindex' => 'notindex.html', }, + $_files_match_directory, + ] + + apache::vhost { 'files.example.net': + docroot => '/var/www/files', + directories => $_directories, + } + file { '/var/www/files/foo': + ensure => directory, + } + file { '/var/www/files/foo/notindex.html': + ensure => file, + content => "Hello Foo\\n", + } + file { '/var/www/files/private.html': + ensure => file, + content => "Hello World\\n", + } + host { 'files.example.net': ip => '127.0.0.1', } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service($service_name) do + it { should be_enabled } + it { should be_running } + end + + it 'should answer to files.example.net' do + shell("/usr/bin/curl -sSf files.example.net:80/").stdout.should eq("Hello World\n") + shell("/usr/bin/curl -sSf files.example.net:80/foo/").stdout.should eq("Hello Foo\n") + shell("/usr/bin/curl -sSf files.example.net:80/private.html", {:acceptable_exit_codes => 22}).stderr.should match(/curl: \(22\) The requested URL returned error: 403/) + end + end + end + + case fact('lsbdistcodename') + when 'precise', 'wheezy' + context 'vhost fallbackresouce example' do + it 'should configure a vhost with Fallbackresource' do + pp = <<-EOS + class { 'apache': } + apache::vhost { 'fallback.example.net': + docroot => '/var/www/fallback', + fallbackresource => '/index.html' + } + file { '/var/www/fallback/index.html': + ensure => file, + content => "Hello World\\n", + } + host { 'fallback.example.net': ip => '127.0.0.1', } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service($service_name) do + it { should be_enabled } + it { should be_running } + end + + it 'should answer to fallback.example.net' do + shell("/usr/bin/curl fallback.example.net:80/Does/Not/Exist") do |r| + r.stdout.should == "Hello World\n" + end + end + + end + else + # The current stable RHEL release (6.4) comes with Apache httpd 2.2.15 + # That was released March 6, 2010. + # FallbackResource was backported to 2.2.16, and released July 25, 2010. + # Ubuntu Lucid (10.04) comes with apache2 2.2.14, released October 3, 2009. + # https://svn.apache.org/repos/asf/httpd/httpd/branches/2.2.x/STATUS + end + + context 'virtual_docroot hosting separate sites' do + it 'should configure a vhost with VirtualDocumentRoot' do + pp = <<-EOS + class { 'apache': } + apache::vhost { 'virt.example.com': + vhost_name => '*', + serveraliases => '*virt.example.com', + port => '80', + docroot => '/var/www/virt', + virtual_docroot => '/var/www/virt/%1', + } + host { 'virt.example.com': ip => '127.0.0.1', } + host { 'a.virt.example.com': ip => '127.0.0.1', } + host { 'b.virt.example.com': ip => '127.0.0.1', } + file { [ '/var/www/virt/a', '/var/www/virt/b', ]: ensure => directory, } + file { '/var/www/virt/a/index.html': ensure => file, content => "Hello from a.virt\\n", } + file { '/var/www/virt/b/index.html': ensure => file, content => "Hello from b.virt\\n", } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe service($service_name) do + it { should be_enabled } + it { should be_running } + end + + it 'should answer to a.virt.example.com' do + shell("/usr/bin/curl a.virt.example.com:80", {:acceptable_exit_codes => 0}) do |r| + r.stdout.should == "Hello from a.virt\n" + end + end + + it 'should answer to b.virt.example.com' do + shell("/usr/bin/curl b.virt.example.com:80", {:acceptable_exit_codes => 0}) do |r| + r.stdout.should == "Hello from b.virt\n" + end + end + end + + context 'proxy_pass for alternative vhost' do + it 'should configure a local vhost and a proxy vhost' do + apply_manifest(%{ + class { 'apache': default_vhost => false, } + apache::vhost { 'localhost': + docroot => '/var/www/local', + ip => '127.0.0.1', + port => '8888', + } + apache::listen { '*:80': } + apache::vhost { 'proxy.example.com': + docroot => '/var/www', + port => '80', + add_listen => false, + proxy_pass => { + 'path' => '/', + 'url' => 'http://localhost:8888/subdir/', + }, + } + host { 'proxy.example.com': ip => '127.0.0.1', } + file { ['/var/www/local', '/var/www/local/subdir']: ensure => directory, } + file { '/var/www/local/subdir/index.html': + ensure => file, + content => "Hello from localhost\\n", + } + }, :catch_failures => true) + end + + describe service($service_name) do + it { should be_enabled } + it { should be_running } + end + + it 'should get a response from the back end' do + shell("/usr/bin/curl --max-redirs 0 proxy.example.com:80") do |r| + r.stdout.should == "Hello from localhost\n" + r.exit_code.should == 0 + end + end + end + + describe 'ip_based' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + ip_based => true, + servername => 'test.server', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file($ports_file) do + it { should be_file } + it { should_not contain 'NameVirtualHost test.server' } + end + end + + describe 'add_listen' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': default_vhost => false } + host { 'testlisten.server': ip => '127.0.0.1' } + apache::listen { '81': } + apache::vhost { 'testlisten.server': + docroot => '/tmp', + port => '80', + add_listen => false, + servername => 'testlisten.server', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file($ports_file) do + it { should be_file } + it { should_not contain 'Listen 80' } + it { should contain 'Listen 81' } + end + end + + describe 'docroot' do + it 'applies cleanly' do + pp = <<-EOS + user { 'test_owner': ensure => present, } + group { 'test_group': ensure => present, } + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp/test', + docroot_owner => 'test_owner', + docroot_group => 'test_group', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file('/tmp/test') do + it { should be_directory } + it { should be_owned_by 'test_owner' } + it { should be_grouped_into 'test_group' } + end + end + + describe 'default_vhost' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + default_vhost => true, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/10-test.server.conf") do + it { should be_file } + end + end + + describe 'options' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + options => ['Indexes','FollowSymLinks', 'ExecCGI'], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'Options Indexes FollowSymLinks ExecCGI' } + end + end + + describe 'override' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + override => ['All'], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'AllowOverride All' } + end + end + + describe 'logroot' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + logroot => '/tmp', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain ' CustomLog "/tmp' } + end + end + + ['access', 'error'].each do |logtype| + case logtype + when 'access' + logname = 'CustomLog' + when 'error' + logname = 'ErrorLog' + end + + describe "#{logtype}_log" do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + logroot => '/tmp', + #{logtype}_log => false, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should_not contain " #{logname} \"/tmp" } + end + end + + describe "#{logtype}_log_pipe" do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + logroot => '/tmp', + #{logtype}_log_pipe => '|/bin/sh', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain " #{logname} \"|/bin/sh" } + end + end + + describe "#{logtype}_log_syslog" do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + logroot => '/tmp', + #{logtype}_log_syslog => 'syslog', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain " #{logname} \"syslog\"" } + end + end + end + + describe 'access_log_format' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + logroot => '/tmp', + access_log_syslog => 'syslog', + access_log_format => '%h %l', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'CustomLog "syslog" "%h %l"' } + end + end + + describe 'access_log_env_var' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + logroot => '/tmp', + access_log_syslog => 'syslog', + access_log_env_var => 'admin', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'CustomLog "syslog" combined env=admin' } + end + end + + describe 'aliases' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + aliases => [{ alias => '/image', path => '/ftp/pub/image' }], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'Alias /image "/ftp/pub/image"' } + end + end + + describe 'scriptaliases' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + scriptaliases => [{ alias => '/myscript', path => '/usr/share/myscript', }], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'ScriptAlias /myscript "/usr/share/myscript"' } + end + end + + describe 'proxy' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': service_ensure => stopped, } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + proxy_dest => 'test2', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'ProxyPass / test2/' } + end + end + + describe 'suphp' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': service_ensure => stopped, } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + suphp_addhandler => '#{$suphp_handler}', + suphp_engine => 'on', + suphp_configpath => '#{$suphp_configpath}', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain "suPHP_AddHandler #{$suphp_handler}" } + it { should contain 'suPHP_Engine on' } + it { should contain "suPHP_ConfigPath \"#{$suphp_configpath}\"" } + end + end + + describe 'no_proxy_uris' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': service_ensure => stopped, } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + proxy_dest => 'http://test2', + no_proxy_uris => [ 'http://test2/test' ], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'ProxyPass / http://test2/' } + it { should contain 'ProxyPass http://test2/test !' } + end + end + + describe 'redirect' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + redirect_source => ['/images'], + redirect_dest => ['http://test.server/'], + redirect_status => ['permanent'], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'Redirect permanent /images http://test.server/' } + end + end + + # Passenger isn't even in EPEL on el-5 + if default['platform'] !~ /^el-5/ + describe 'rack_base_uris' do + if fact('osfamily') == 'RedHat' + it 'adds epel' do + pp = "class { 'epel': }" + apply_manifest(pp, :catch_failures => true) + end + end + + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + rack_base_uris => ['/test'], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'RackBaseURI /test' } + end + end + end + + + describe 'request_headers' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + request_headers => ['append MirrorID "mirror 12"'], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'append MirrorID "mirror 12"' } + end + end + + describe 'rewrite rules' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + rewrites => [ + { comment => 'test', + rewrite_cond => '%{HTTP_USER_AGENT} ^Lynx/ [OR]', + rewrite_rule => ['^index\.html$ welcome.html'], + } + ], + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain '#test' } + it { should contain 'RewriteCond %{HTTP_USER_AGENT} ^Lynx/ [OR]' } + it { should contain 'RewriteRule ^index.html$ welcome.html' } + end + end + + describe 'setenv/setenvif' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + setenv => ['TEST /test'], + setenvif => ['Request_URI "\.gif$" object_is_image=gif'] + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'SetEnv TEST /test' } + it { should contain 'SetEnvIf Request_URI "\.gif$" object_is_image=gif' } + end + end + + describe 'block' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + block => 'scm', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain '' } + end + end + + describe 'wsgi' do + it 'import_script applies cleanly' do + pp = <<-EOS + class { 'apache': } + class { 'apache::mod::wsgi': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + wsgi_application_group => '%{GLOBAL}', + wsgi_daemon_process => 'wsgi', + wsgi_daemon_process_options => {processes => '2'}, + wsgi_process_group => 'nobody', + wsgi_script_aliases => { '/test' => '/test1' }, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + it 'import_script applies cleanly', :unless => fact('lsbcodename') == 'lucid' do + pp = <<-EOS + class { 'apache': } + class { 'apache::mod::wsgi': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + wsgi_application_group => '%{GLOBAL}', + wsgi_daemon_process => 'wsgi', + wsgi_daemon_process_options => {processes => '2'}, + wsgi_import_script => '/test1', + wsgi_import_script_options => { application-group => '%{GLOBAL}', process-group => 'wsgi' }, + wsgi_process_group => 'nobody', + wsgi_script_aliases => { '/test' => '/test1' }, + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'WSGIApplicationGroup %{GLOBAL}' } + it { should contain 'WSGIDaemonProcess wsgi processes=2' } + it { should contain 'WSGIImportScript /test1 application-group=%{GLOBAL} process-group=wsgi' } + it { should contain 'WSGIProcessGroup nobody' } + it { should contain 'WSGIScriptAlias /test "/test1"' } + end + end + + describe 'custom_fragment' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + custom_fragment => inline_template('#weird test string'), + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain '#weird test string' } + end + end + + describe 'itk' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + itk => { user => 'nobody', group => 'nobody' } + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'AssignUserId nobody nobody' } + end + end + + # So what does this work on? + if default['platform'] !~ /^(debian-(6|7)|el-(5|6))/ + describe 'fastcgi' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + class { 'apache::mod::fastcgi': } + host { 'test.server': ip => '127.0.0.1' } + apache::vhost { 'test.server': + docroot => '/tmp', + fastcgi_server => 'localhost', + fastcgi_socket => '/tmp/fast/1234', + fastcgi_dir => '/tmp/fast', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'FastCgiExternalServer localhost -socket /tmp/fast/1234' } + it { should contain '' } + end + end + end + + describe 'additional_includes' do + it 'applies cleanly' do + pp = <<-EOS + class { 'apache': } + host { 'test.server': ip => '127.0.0.1' } + file { '/tmp/include': ensure => present, content => '#additional_includes' } + apache::vhost { 'test.server': + docroot => '/tmp', + additional_includes => '/tmp/include', + } + EOS + apply_manifest(pp, :catch_failures => true) + end + + describe file("#{$vhost_dir}/25-test.server.conf") do + it { should be_file } + it { should contain 'Include "/tmp/include"' } + end + end + +end diff --git a/apache/spec/classes/apache_spec.rb b/apache/spec/classes/apache_spec.rb new file mode 100644 index 000000000..1a9a58d1b --- /dev/null +++ b/apache/spec/classes/apache_spec.rb @@ -0,0 +1,496 @@ +require 'spec_helper' + +describe 'apache', :type => :class do + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_package("httpd").with( + 'notify' => 'Class[Apache::Service]', + 'ensure' => 'installed' + ) + } + it { should contain_user("www-data") } + it { should contain_group("www-data") } + it { should contain_class("apache::service") } + it { should contain_file("/etc/apache2/sites-enabled").with( + 'ensure' => 'directory', + 'recurse' => 'true', + 'purge' => 'true', + 'notify' => 'Class[Apache::Service]', + 'require' => 'Package[httpd]' + ) + } + it { should contain_file("/etc/apache2/mods-enabled").with( + 'ensure' => 'directory', + 'recurse' => 'true', + 'purge' => 'true', + 'notify' => 'Class[Apache::Service]', + 'require' => 'Package[httpd]' + ) + } + it { should contain_file("/etc/apache2/mods-available").with( + 'ensure' => 'directory', + 'recurse' => 'true', + 'purge' => 'false', + 'notify' => 'Class[Apache::Service]', + 'require' => 'Package[httpd]' + ) + } + it { should contain_concat("/etc/apache2/ports.conf").with( + 'owner' => 'root', + 'group' => 'root', + 'mode' => '0644', + 'notify' => 'Class[Apache::Service]' + ) + } + # Assert that load files are placed and symlinked for these mods, but no conf file. + [ + 'auth_basic', + 'authn_file', + 'authz_default', + 'authz_groupfile', + 'authz_host', + 'authz_user', + 'dav', + 'env' + ].each do |modname| + it { should contain_file("#{modname}.load").with( + 'path' => "/etc/apache2/mods-available/#{modname}.load", + 'ensure' => 'file' + ) } + it { should contain_file("#{modname}.load symlink").with( + 'path' => "/etc/apache2/mods-enabled/#{modname}.load", + 'ensure' => 'link', + 'target' => "/etc/apache2/mods-available/#{modname}.load" + ) } + it { should_not contain_file("#{modname}.conf") } + it { should_not contain_file("#{modname}.conf symlink") } + end + + context "with Apache version < 2.4" do + let :params do + { :apache_version => 2.2 } + end + + it { should contain_file("/etc/apache2/apache2.conf").with_content %r{^Include "/etc/apache2/conf\.d/\*\.conf"$} } + end + + context "with Apache version >= 2.4" do + let :params do + { :apache_version => 2.4 } + end + + it { should contain_file("/etc/apache2/apache2.conf").with_content %r{^IncludeOptional "/etc/apache2/conf\.d/\*\.conf"$} } + end + + # Assert that both load files and conf files are placed and symlinked for these mods + [ + 'alias', + 'autoindex', + 'dav_fs', + 'deflate', + 'dir', + 'mime', + 'negotiation', + 'setenvif', + ].each do |modname| + it { should contain_file("#{modname}.load").with( + 'path' => "/etc/apache2/mods-available/#{modname}.load", + 'ensure' => 'file' + ) } + it { should contain_file("#{modname}.load symlink").with( + 'path' => "/etc/apache2/mods-enabled/#{modname}.load", + 'ensure' => 'link', + 'target' => "/etc/apache2/mods-available/#{modname}.load" + ) } + it { should contain_file("#{modname}.conf").with( + 'path' => "/etc/apache2/mods-available/#{modname}.conf", + 'ensure' => 'file' + ) } + it { should contain_file("#{modname}.conf symlink").with( + 'path' => "/etc/apache2/mods-enabled/#{modname}.conf", + 'ensure' => 'link', + 'target' => "/etc/apache2/mods-available/#{modname}.conf" + ) } + end + + describe "Don't create user resource" do + context "when parameter manage_user is false" do + let :params do + { :manage_user => false } + end + + it { should_not contain_user('www-data') } + it { should contain_file("/etc/apache2/apache2.conf").with_content %r{^User www-data\n} } + end + end + describe "Don't create group resource" do + context "when parameter manage_group is false" do + let :params do + { :manage_group => false } + end + + it { should_not contain_group('www-data') } + it { should contain_file("/etc/apache2/apache2.conf").with_content %r{^Group www-data\n} } + end + end + end + context "on a RedHat 5 OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '5', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_package("httpd").with( + 'notify' => 'Class[Apache::Service]', + 'ensure' => 'installed' + ) + } + it { should contain_user("apache") } + it { should contain_group("apache") } + it { should contain_class("apache::service") } + it { should contain_file("/etc/httpd/conf.d").with( + 'ensure' => 'directory', + 'recurse' => 'true', + 'purge' => 'true', + 'notify' => 'Class[Apache::Service]', + 'require' => 'Package[httpd]' + ) + } + it { should contain_concat("/etc/httpd/conf/ports.conf").with( + 'owner' => 'root', + 'group' => 'root', + 'mode' => '0644', + 'notify' => 'Class[Apache::Service]' + ) + } + describe "Alternate confd/mod/vhosts directory" do + let :params do + { + :vhost_dir => '/etc/httpd/site.d', + :confd_dir => '/etc/httpd/conf.d', + :mod_dir => '/etc/httpd/mod.d', + } + end + + ['mod.d','site.d','conf.d'].each do |dir| + it { should contain_file("/etc/httpd/#{dir}").with( + 'ensure' => 'directory', + 'recurse' => 'true', + 'purge' => 'true', + 'notify' => 'Class[Apache::Service]', + 'require' => 'Package[httpd]' + ) } + end + + # Assert that load files are placed for these mods, but no conf file. + [ + 'auth_basic', + 'authn_file', + 'authz_default', + 'authz_groupfile', + 'authz_host', + 'authz_user', + 'dav', + 'env', + ].each do |modname| + it { should contain_file("#{modname}.load").with_path( + "/etc/httpd/mod.d/#{modname}.load" + ) } + it { should_not contain_file("#{modname}.conf").with_path( + "/etc/httpd/mod.d/#{modname}.conf" + ) } + end + + # Assert that both load files and conf files are placed for these mods + [ + 'alias', + 'autoindex', + 'dav_fs', + 'deflate', + 'dir', + 'mime', + 'negotiation', + 'setenvif', + ].each do |modname| + it { should contain_file("#{modname}.load").with_path( + "/etc/httpd/mod.d/#{modname}.load" + ) } + it { should contain_file("#{modname}.conf").with_path( + "/etc/httpd/mod.d/#{modname}.conf" + ) } + end + + context "with Apache version < 2.4" do + let :params do + { :apache_version => 2.2 } + end + + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^Include "/etc/httpd/conf\.d/\*\.conf"$} } + end + + context "with Apache version >= 2.4" do + let :params do + { :apache_version => 2.4 } + end + + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^IncludeOptional "/etc/httpd/conf\.d/\*\.conf"$} } + end + + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^Include "/etc/httpd/site\.d/\*\.conf"$} } + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^Include "/etc/httpd/mod\.d/\*\.conf"$} } + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^Include "/etc/httpd/mod\.d/\*\.load"$} } + end + + describe "Alternate conf.d directory" do + let :params do + { :confd_dir => '/etc/httpd/special_conf.d' } + end + + it { should contain_file("/etc/httpd/special_conf.d").with( + 'ensure' => 'directory', + 'recurse' => 'true', + 'purge' => 'true', + 'notify' => 'Class[Apache::Service]', + 'require' => 'Package[httpd]' + ) } + end + + describe "Alternate mpm_modules" do + context "when declaring mpm_module is false" do + let :params do + { :mpm_module => false } + end + it 'should not declare mpm modules' do + should_not contain_class('apache::mod::event') + should_not contain_class('apache::mod::itk') + should_not contain_class('apache::mod::peruser') + should_not contain_class('apache::mod::prefork') + should_not contain_class('apache::mod::worker') + end + end + context "when declaring mpm_module => prefork" do + let :params do + { :mpm_module => 'prefork' } + end + it { should contain_class('apache::mod::prefork') } + it { should_not contain_class('apache::mod::event') } + it { should_not contain_class('apache::mod::itk') } + it { should_not contain_class('apache::mod::peruser') } + it { should_not contain_class('apache::mod::worker') } + end + context "when declaring mpm_module => worker" do + let :params do + { :mpm_module => 'worker' } + end + it { should contain_class('apache::mod::worker') } + it { should_not contain_class('apache::mod::event') } + it { should_not contain_class('apache::mod::itk') } + it { should_not contain_class('apache::mod::peruser') } + it { should_not contain_class('apache::mod::prefork') } + end + context "when declaring mpm_module => breakme" do + let :params do + { :mpm_module => 'breakme' } + end + it { expect { subject }.to raise_error Puppet::Error, /does not match/ } + end + end + + describe "different templates for httpd.conf" do + context "with default" do + let :params do + { :conf_template => 'apache/httpd.conf.erb' } + end + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^# Security\n} } + end + context "with non-default" do + let :params do + { :conf_template => 'site_apache/fake.conf.erb' } + end + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^Fake template for rspec.$} } + end + end + + describe "default mods" do + context "without" do + let :params do + { :default_mods => false } + end + + it { should contain_apache__mod('authz_host') } + it { should_not contain_apache__mod('env') } + end + context "custom" do + let :params do + { :default_mods => [ + 'info', + 'alias', + 'mime', + 'env', + 'setenv', + 'expires', + ]} + end + + it { should contain_apache__mod('authz_host') } + it { should contain_apache__mod('env') } + it { should contain_class('apache::mod::info') } + it { should contain_class('apache::mod::mime') } + end + end + describe "Don't create user resource" do + context "when parameter manage_user is false" do + let :params do + { :manage_user => false } + end + + it { should_not contain_user('apache') } + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^User apache\n} } + end + end + describe "Don't create group resource" do + context "when parameter manage_group is false" do + let :params do + { :manage_group => false } + end + + it { should_not contain_group('apache') } + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^Group apache\n} } + + end + end + describe "sendfile" do + context "with invalid value" do + let :params do + { :sendfile => 'foo' } + end + it "should fail" do + expect do + subject + end.to raise_error(Puppet::Error, /"foo" does not match/) + end + end + context "On" do + let :params do + { :sendfile => 'On' } + end + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^EnableSendfile On\n} } + end + context "Off" do + let :params do + { :sendfile => 'Off' } + end + it { should contain_file("/etc/httpd/conf/httpd.conf").with_content %r{^EnableSendfile Off\n} } + end + end + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_class("apache::package").with({'ensure' => 'present'}) } + it { should contain_user("www") } + it { should contain_group("www") } + it { should contain_class("apache::service") } + it { should contain_file("/usr/local/etc/apache22/Vhosts").with( + 'ensure' => 'directory', + 'recurse' => 'true', + 'purge' => 'true', + 'notify' => 'Class[Apache::Service]', + 'require' => 'Package[httpd]' + ) } + it { should contain_file("/usr/local/etc/apache22/Modules").with( + 'ensure' => 'directory', + 'recurse' => 'true', + 'purge' => 'true', + 'notify' => 'Class[Apache::Service]', + 'require' => 'Package[httpd]' + ) } + it { should contain_concat("/usr/local/etc/apache22/ports.conf").with( + 'owner' => 'root', + 'group' => 'wheel', + 'mode' => '0644', + 'notify' => 'Class[Apache::Service]' + ) } + # Assert that load files are placed for these mods, but no conf file. + [ + 'auth_basic', + 'authn_file', + 'authz_default', + 'authz_groupfile', + 'authz_host', + 'authz_user', + 'dav', + 'env' + ].each do |modname| + it { should contain_file("#{modname}.load").with( + 'path' => "/usr/local/etc/apache22/Modules/#{modname}.load", + 'ensure' => 'file' + ) } + it { should_not contain_file("#{modname}.conf") } + end + + # Assert that both load files and conf files are placed for these mods + [ + 'alias', + 'autoindex', + 'dav_fs', + 'deflate', + 'dir', + 'mime', + 'negotiation', + 'setenvif', + ].each do |modname| + it { should contain_file("#{modname}.load").with( + 'path' => "/usr/local/etc/apache22/Modules/#{modname}.load", + 'ensure' => 'file' + ) } + it { should contain_file("#{modname}.conf").with( + 'path' => "/usr/local/etc/apache22/Modules/#{modname}.conf", + 'ensure' => 'file' + ) } + end + end + context 'on all OSes' do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + context 'default vhost defaults' do + it { should contain_apache__vhost('default').with_ensure('present') } + it { should contain_apache__vhost('default-ssl').with_ensure('absent') } + end + context 'without default non-ssl vhost' do + let :params do { + :default_vhost => false + } + end + it { should contain_apache__vhost('default').with_ensure('absent') } + end + context 'with default ssl vhost' do + let :params do { + :default_ssl_vhost => true + } + end + it { should contain_apache__vhost('default-ssl').with_ensure('present') } + end + end +end diff --git a/apache/spec/classes/dev_spec.rb b/apache/spec/classes/dev_spec.rb new file mode 100644 index 000000000..e3d7dee39 --- /dev/null +++ b/apache/spec/classes/dev_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'apache::dev', :type => :class do + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + } + end + it { should contain_class("apache::params") } + it { should contain_package("libaprutil1-dev") } + it { should contain_package("libapr1-dev") } + it { should contain_package("apache2-prefork-dev") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + } + end + it { should contain_class("apache::params") } + it { should contain_package("httpd-devel") } + end + context "on a FreeBSD OS" do + let :pre_condition do + 'include apache::package' + end + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + } + end + it { should contain_class("apache::params") } + end +end diff --git a/apache/spec/classes/mod/auth_kerb_spec.rb b/apache/spec/classes/mod/auth_kerb_spec.rb new file mode 100644 index 000000000..71c2349c3 --- /dev/null +++ b/apache/spec/classes/mod/auth_kerb_spec.rb @@ -0,0 +1,41 @@ +describe 'apache::mod::auth_kerb', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod("auth_kerb") } + it { should contain_package("libapache2-mod-auth-kerb") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod("auth_kerb") } + it { should contain_package("mod_auth_kerb") } + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod("auth_kerb") } + it { should contain_package("www/mod_auth_kerb2") } + end +end diff --git a/apache/spec/classes/mod/authnz_ldap_spec.rb b/apache/spec/classes/mod/authnz_ldap_spec.rb new file mode 100644 index 000000000..c8e832d95 --- /dev/null +++ b/apache/spec/classes/mod/authnz_ldap_spec.rb @@ -0,0 +1,65 @@ +describe 'apache::mod::authnz_ldap', :type => :class do + let :pre_condition do + 'include apache' + end + + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_class("apache::mod::ldap") } + it { should contain_apache__mod('authnz_ldap') } + + context 'default verifyServerCert' do + it { should contain_file('authnz_ldap.conf').with_content(/^LDAPVerifyServerCert On$/) } + end + + context 'verifyServerCert = false' do + let(:params) { { :verifyServerCert => false } } + it { should contain_file('authnz_ldap.conf').with_content(/^LDAPVerifyServerCert Off$/) } + end + + context 'verifyServerCert = wrong' do + let(:params) { { :verifyServerCert => 'wrong' } } + it 'should raise an error' do + expect { should raise_error Puppet::Error } + end + end + end #Debian + + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_class("apache::mod::ldap") } + it { should contain_apache__mod('authnz_ldap') } + + context 'default verifyServerCert' do + it { should contain_file('authnz_ldap.conf').with_content(/^LDAPVerifyServerCert On$/) } + end + + context 'verifyServerCert = false' do + let(:params) { { :verifyServerCert => false } } + it { should contain_file('authnz_ldap.conf').with_content(/^LDAPVerifyServerCert Off$/) } + end + + context 'verifyServerCert = wrong' do + let(:params) { { :verifyServerCert => 'wrong' } } + it 'should raise an error' do + expect { should raise_error Puppet::Error } + end + end + end # Redhat + +end + diff --git a/apache/spec/classes/mod/dav_svn_spec.rb b/apache/spec/classes/mod/dav_svn_spec.rb new file mode 100644 index 000000000..fe11bb8cb --- /dev/null +++ b/apache/spec/classes/mod/dav_svn_spec.rb @@ -0,0 +1,41 @@ +describe 'apache::mod::dav_svn', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('dav_svn') } + it { should contain_package("libapache2-svn") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('dav_svn') } + it { should contain_package("mod_dav_svn") } + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('dav_svn') } + it { should contain_package("devel/subversion") } + end +end diff --git a/apache/spec/classes/mod/dev_spec.rb b/apache/spec/classes/mod/dev_spec.rb new file mode 100644 index 000000000..b72217aad --- /dev/null +++ b/apache/spec/classes/mod/dev_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' +describe 'apache::mod::dev', :type => :class do + [ + ['RedHat', '6'], + ['Debian', '6'], + ['FreeBSD', '9'], + ].each do |osfamily, operatingsystemrelease| + if osfamily == 'FreeBSD' + let :pre_condition do + 'include apache::package' + end + end + context "on a #{osfamily} OS" do + let :facts do + { + :osfamily => osfamily, + :operatingsystemrelease => operatingsystemrelease, + } + end + it { should contain_class('apache::dev') } + end + end +end diff --git a/apache/spec/classes/mod/dir_spec.rb b/apache/spec/classes/mod/dir_spec.rb new file mode 100644 index 000000000..b195eda0f --- /dev/null +++ b/apache/spec/classes/mod/dir_spec.rb @@ -0,0 +1,88 @@ +describe 'apache::mod::dir', :type => :class do + let :pre_condition do + 'class { "apache": + default_mods => false, + }' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + context "passing no parameters" do + it { should contain_class("apache::params") } + it { should contain_apache__mod('dir') } + it { should contain_file('dir.conf').with_content(/^DirectoryIndex /) } + it { should contain_file('dir.conf').with_content(/ index\.html /) } + it { should contain_file('dir.conf').with_content(/ index\.html\.var /) } + it { should contain_file('dir.conf').with_content(/ index\.cgi /) } + it { should contain_file('dir.conf').with_content(/ index\.pl /) } + it { should contain_file('dir.conf').with_content(/ index\.php /) } + it { should contain_file('dir.conf').with_content(/ index\.xhtml$/) } + end + context "passing indexes => ['example.txt','fearsome.aspx']" do + let :params do + {:indexes => ['example.txt','fearsome.aspx']} + end + it { should contain_file('dir.conf').with_content(/ example\.txt /) } + it { should contain_file('dir.conf').with_content(/ fearsome\.aspx$/) } + end + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + context "passing no parameters" do + it { should contain_class("apache::params") } + it { should contain_apache__mod('dir') } + it { should contain_file('dir.conf').with_content(/^DirectoryIndex /) } + it { should contain_file('dir.conf').with_content(/ index\.html /) } + it { should contain_file('dir.conf').with_content(/ index\.html\.var /) } + it { should contain_file('dir.conf').with_content(/ index\.cgi /) } + it { should contain_file('dir.conf').with_content(/ index\.pl /) } + it { should contain_file('dir.conf').with_content(/ index\.php /) } + it { should contain_file('dir.conf').with_content(/ index\.xhtml$/) } + end + context "passing indexes => ['example.txt','fearsome.aspx']" do + let :params do + {:indexes => ['example.txt','fearsome.aspx']} + end + it { should contain_file('dir.conf').with_content(/ example\.txt /) } + it { should contain_file('dir.conf').with_content(/ fearsome\.aspx$/) } + end + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + context "passing no parameters" do + it { should contain_class("apache::params") } + it { should contain_apache__mod('dir') } + it { should contain_file('dir.conf').with_content(/^DirectoryIndex /) } + it { should contain_file('dir.conf').with_content(/ index\.html /) } + it { should contain_file('dir.conf').with_content(/ index\.html\.var /) } + it { should contain_file('dir.conf').with_content(/ index\.cgi /) } + it { should contain_file('dir.conf').with_content(/ index\.pl /) } + it { should contain_file('dir.conf').with_content(/ index\.php /) } + it { should contain_file('dir.conf').with_content(/ index\.xhtml$/) } + end + context "passing indexes => ['example.txt','fearsome.aspx']" do + let :params do + {:indexes => ['example.txt','fearsome.aspx']} + end + it { should contain_file('dir.conf').with_content(/ example\.txt /) } + it { should contain_file('dir.conf').with_content(/ fearsome\.aspx$/) } + end + end +end diff --git a/apache/spec/classes/mod/event_spec.rb b/apache/spec/classes/mod/event_spec.rb new file mode 100644 index 000000000..320374a00 --- /dev/null +++ b/apache/spec/classes/mod/event_spec.rb @@ -0,0 +1,88 @@ +describe 'apache::mod::event', :type => :class do + let :pre_condition do + 'class { "apache": mpm_module => false, }' + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('event') } + it { should contain_file("/usr/local/etc/apache22/Modules/event.conf").with_ensure('file') } + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('event') } + it { should contain_file("/etc/apache2/mods-available/event.conf").with_ensure('file') } + it { should contain_file("/etc/apache2/mods-enabled/event.conf").with_ensure('link') } + + context "with Apache version < 2.4" do + let :params do + { + :apache_version => 2.2, + } + end + + it { should_not contain_file("/etc/apache2/mods-available/event.load") } + it { should_not contain_file("/etc/apache2/mods-enabled/event.load") } + + it { should contain_package("apache2-mpm-event") } + end + + context "with Apache version >= 2.4" do + let :params do + { + :apache_version => 2.4, + } + end + + it { should contain_file("/etc/apache2/mods-available/event.load").with({ + 'ensure' => 'file', + 'content' => "LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so\n" + }) + } + it { should contain_file("/etc/apache2/mods-enabled/event.load").with_ensure('link') } + end + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + context "with Apache version >= 2.4" do + let :params do + { + :apache_version => 2.4, + } + end + + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('worker') } + it { should_not contain_apache__mod('prefork') } + + it { should contain_file("/etc/httpd/conf.d/event.conf").with_ensure('file') } + + it { should contain_file("/etc/httpd/conf.d/event.load").with({ + 'ensure' => 'file', + 'content' => "LoadModule mpm_event_module modules/mod_mpm_event.so\n", + }) + } + end + end +end diff --git a/apache/spec/classes/mod/fastcgi_spec.rb b/apache/spec/classes/mod/fastcgi_spec.rb new file mode 100644 index 000000000..8138bbab7 --- /dev/null +++ b/apache/spec/classes/mod/fastcgi_spec.rb @@ -0,0 +1,32 @@ +describe 'apache::mod::fastcgi', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('fastcgi') } + it { should contain_package("libapache2-mod-fastcgi") } + it { should contain_file('fastcgi.conf') } + end + + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('fastcgi') } + it { should contain_package("mod_fastcgi") } + it { should_not contain_file('fastcgi.conf') } + end +end diff --git a/apache/spec/classes/mod/fcgid_spec.rb b/apache/spec/classes/mod/fcgid_spec.rb new file mode 100644 index 000000000..5cc337291 --- /dev/null +++ b/apache/spec/classes/mod/fcgid_spec.rb @@ -0,0 +1,41 @@ +describe 'apache::mod::fcgid', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('fcgid') } + it { should contain_package("libapache2-mod-fcgid") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('fcgid') } + it { should contain_package("mod_fcgid") } + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('fcgid') } + it { should contain_package("www/mod_fcgid") } + end +end diff --git a/apache/spec/classes/mod/info_spec.rb b/apache/spec/classes/mod/info_spec.rb new file mode 100644 index 000000000..21d253e98 --- /dev/null +++ b/apache/spec/classes/mod/info_spec.rb @@ -0,0 +1,99 @@ +# This function is called inside the OS specific contexts +def general_info_specs + it { should contain_apache__mod("info") } + + it do + should contain_file("info.conf").with_content( + "\n"\ + " SetHandler server-info\n"\ + " Order deny,allow\n"\ + " Deny from all\n"\ + " Allow from 127.0.0.1 ::1\n"\ + "\n" + ) + end +end + +describe 'apache::mod::info', :type => :class do + let :pre_condition do + 'include apache' + end + + context "On a Debian OS with default params" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + # Load the more generic tests for this context + general_info_specs() + + it { should contain_file("info.conf").with({ + :ensure => 'file', + :path => '/etc/apache2/mods-available/info.conf', + } ) } + it { should contain_file("info.conf symlink").with({ + :ensure => 'link', + :path => '/etc/apache2/mods-enabled/info.conf', + } ) } + end + + context "on a RedHat OS with default params" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + # Load the more generic tests for this context + general_info_specs() + + it { should contain_file("info.conf").with_path("/etc/httpd/conf.d/info.conf") } + end + + context "On a FreeBSD OS with default params" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + + # Load the more generic tests for this context + general_info_specs() + + it { should contain_file("info.conf").with({ + :ensure => 'file', + :path => '/usr/local/etc/apache22/Modules/info.conf', + } ) } + end + + context "with $allow_from => ['10.10.10.10','11.11.11.11']" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + let :params do + { :allow_from => ['10.10.10.10','11.11.11.11'] } + end + it do + should contain_file("info.conf").with_content( + "\n"\ + " SetHandler server-info\n"\ + " Order deny,allow\n"\ + " Deny from all\n"\ + " Allow from 10.10.10.10 11.11.11.11\n"\ + "\n" + ) + end + end +end diff --git a/apache/spec/classes/mod/itk_spec.rb b/apache/spec/classes/mod/itk_spec.rb new file mode 100644 index 000000000..032e122d4 --- /dev/null +++ b/apache/spec/classes/mod/itk_spec.rb @@ -0,0 +1,58 @@ +describe 'apache::mod::itk', :type => :class do + let :pre_condition do + 'class { "apache": mpm_module => false, }' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('itk') } + it { should contain_file("/etc/apache2/mods-available/itk.conf").with_ensure('file') } + it { should contain_file("/etc/apache2/mods-enabled/itk.conf").with_ensure('link') } + + context "with Apache version < 2.4" do + let :params do + { + :apache_version => 2.2, + } + end + + it { should_not contain_file("/etc/apache2/mods-available/itk.load") } + it { should_not contain_file("/etc/apache2/mods-enabled/itk.load") } + + it { should contain_package("apache2-mpm-itk") } + end + + context "with Apache version >= 2.4" do + let :params do + { + :apache_version => 2.4, + } + end + + it { should contain_file("/etc/apache2/mods-available/itk.load").with({ + 'ensure' => 'file', + 'content' => "LoadModule mpm_itk_module /usr/lib/apache2/modules/mod_mpm_itk.so\n" + }) + } + it { should contain_file("/etc/apache2/mods-enabled/itk.load").with_ensure('link') } + end + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('itk') } + it { should contain_file("/usr/local/etc/apache22/Modules/itk.conf").with_ensure('file') } + end +end diff --git a/apache/spec/classes/mod/mime_magic_spec.rb b/apache/spec/classes/mod/mime_magic_spec.rb new file mode 100644 index 000000000..d10d8345b --- /dev/null +++ b/apache/spec/classes/mod/mime_magic_spec.rb @@ -0,0 +1,93 @@ +# This function is called inside the OS specific contexts +def general_mime_magic_specs + it { should contain_apache__mod("mime_magic") } +end + +describe 'apache::mod::mime_magic', :type => :class do + let :pre_condition do + 'include apache' + end + + context "On a Debian OS with default params" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + general_mime_magic_specs() + + it do + should contain_file("mime_magic.conf").with_content( + "MIMEMagicFile \"/etc/apache2/magic\"\n" + ) + end + + it { should contain_file("mime_magic.conf").with({ + :ensure => 'file', + :path => '/etc/apache2/mods-available/mime_magic.conf', + } ) } + it { should contain_file("mime_magic.conf symlink").with({ + :ensure => 'link', + :path => '/etc/apache2/mods-enabled/mime_magic.conf', + } ) } + + context "with magic_file => /tmp/Debian_magic" do + let :params do + { :magic_file => "/tmp/Debian_magic" } + end + + it do + should contain_file("mime_magic.conf").with_content( + "MIMEMagicFile \"/tmp/Debian_magic\"\n" + ) + end + end + + end + + context "on a RedHat OS with default params" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + general_mime_magic_specs() + + it do + should contain_file("mime_magic.conf").with_content( + "MIMEMagicFile \"/etc/httpd/conf/magic\"\n" + ) + end + + it { should contain_file("mime_magic.conf").with_path("/etc/httpd/conf.d/mime_magic.conf") } + + end + + context "with magic_file => /tmp/magic" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + let :params do + { :magic_file => "/tmp/magic" } + end + + it do + should contain_file("mime_magic.conf").with_content( + "MIMEMagicFile \"/tmp/magic\"\n" + ) + end + end + + +end diff --git a/apache/spec/classes/mod/passenger_spec.rb b/apache/spec/classes/mod/passenger_spec.rb new file mode 100644 index 000000000..37177f477 --- /dev/null +++ b/apache/spec/classes/mod/passenger_spec.rb @@ -0,0 +1,129 @@ +describe 'apache::mod::passenger', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('passenger') } + it { should contain_package("libapache2-mod-passenger") } + it { should contain_file('passenger.conf').with({ + 'path' => '/etc/apache2/mods-available/passenger.conf', + }) } + it { should contain_file('passenger.conf').with_content(/^ PassengerRoot "\/usr"$/) } + it { should contain_file('passenger.conf').with_content(/^ PassengerRuby "\/usr\/bin\/ruby"$/) } + describe "with passenger_high_performance => true" do + let :params do + { :passenger_high_performance => 'true' } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerHighPerformance true$/) } + end + describe "with passenger_pool_idle_time => 1200" do + let :params do + { :passenger_pool_idle_time => 1200 } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerPoolIdleTime 1200$/) } + end + describe "with passenger_max_requests => 20" do + let :params do + { :passenger_max_requests => 20 } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerMaxRequests 20$/) } + end + describe "with passenger_stat_throttle_rate => 10" do + let :params do + { :passenger_stat_throttle_rate => 10 } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerStatThrottleRate 10$/) } + end + describe "with passenger_max_pool_size => 16" do + let :params do + { :passenger_max_pool_size => 16 } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerMaxPoolSize 16$/) } + end + describe "with rack_autodetect => true" do + let :params do + { :rack_autodetect => true } + end + it { should contain_file('passenger.conf').with_content(/^ RackAutoDetect true$/) } + end + describe "with rails_autodetect => true" do + let :params do + { :rails_autodetect => true } + end + it { should contain_file('passenger.conf').with_content(/^ RailsAutoDetect true$/) } + end + describe "with passenger_root => '/usr/lib/example'" do + let :params do + { :passenger_root => '/usr/lib/example' } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerRoot "\/usr\/lib\/example"$/) } + end + describe "with passenger_ruby => /user/lib/example/ruby" do + let :params do + { :passenger_ruby => '/user/lib/example/ruby' } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerRuby "\/user\/lib\/example\/ruby"$/) } + end + describe "with passenger_use_global_queue => true" do + let :params do + { :passenger_use_global_queue => 'true' } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerUseGlobalQueue true$/) } + end + + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('passenger') } + it { should contain_package("mod_passenger") } + it { should contain_file('passenger_package.conf').with({ + 'path' => '/etc/httpd/conf.d/passenger.conf', + }) } + it { should contain_file('passenger_package.conf').without_content } + it { should contain_file('passenger_package.conf').without_source } + it { should contain_file('passenger.conf').with({ + 'path' => '/etc/httpd/conf.d/passenger_extra.conf', + }) } + it { should contain_file('passenger.conf').without_content(/PassengerRoot/) } + it { should contain_file('passenger.conf').without_content(/PassengerRuby/) } + describe "with passenger_root => '/usr/lib/example'" do + let :params do + { :passenger_root => '/usr/lib/example' } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerRoot "\/usr\/lib\/example"$/) } + end + describe "with passenger_ruby => /user/lib/example/ruby" do + let :params do + { :passenger_ruby => '/user/lib/example/ruby' } + end + it { should contain_file('passenger.conf').with_content(/^ PassengerRuby "\/user\/lib\/example\/ruby"$/) } + end + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('passenger') } + it { should contain_package("www/rubygem-passenger") } + end +end diff --git a/apache/spec/classes/mod/perl_spec.rb b/apache/spec/classes/mod/perl_spec.rb new file mode 100644 index 000000000..3cb7a3e67 --- /dev/null +++ b/apache/spec/classes/mod/perl_spec.rb @@ -0,0 +1,41 @@ +describe 'apache::mod::perl', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('perl') } + it { should contain_package("libapache2-mod-perl2") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('perl') } + it { should contain_package("mod_perl") } + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('perl') } + it { should contain_package("www/mod_perl2") } + end +end diff --git a/apache/spec/classes/mod/peruser_spec.rb b/apache/spec/classes/mod/peruser_spec.rb new file mode 100644 index 000000000..062905ccc --- /dev/null +++ b/apache/spec/classes/mod/peruser_spec.rb @@ -0,0 +1,17 @@ +describe 'apache::mod::peruser', :type => :class do + let :pre_condition do + 'class { "apache": mpm_module => false, }' + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('peruser') } + it { should contain_file("/usr/local/etc/apache22/Modules/peruser.conf").with_ensure('file') } + end +end diff --git a/apache/spec/classes/mod/php_spec.rb b/apache/spec/classes/mod/php_spec.rb new file mode 100644 index 000000000..57708a398 --- /dev/null +++ b/apache/spec/classes/mod/php_spec.rb @@ -0,0 +1,99 @@ +describe 'apache::mod::php', :type => :class do + describe "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + context "with mpm_module => prefork" do + let :pre_condition do + 'class { "apache": mpm_module => prefork, }' + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('php5') } + it { should contain_package("libapache2-mod-php5") } + it { should contain_file("php5.load").with( + :content => "LoadModule php5_module /usr/lib/apache2/modules/libphp5.so\n" + ) } + end + context 'with mpm_module => worker' do + let :pre_condition do + 'class { "apache": mpm_module => worker, }' + end + it 'should raise an error' do + expect { subject }.to raise_error Puppet::Error, /mpm_module => 'prefork'/ + end + end + end + describe "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + context "with default params" do + let :pre_condition do + 'class { "apache": }' + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('php5') } + it { should contain_package("php") } + it { should contain_file("php5.load").with( + :content => "LoadModule php5_module modules/libphp5.so\n" + ) } + end + context "with specific version" do + let :pre_condition do + 'class { "apache": }' + end + let :params do + { :package_ensure => '5.3.13'} + end + it { should contain_package("php").with( + :ensure => '5.3.13' + ) } + end + context "with mpm_module => prefork" do + let :pre_condition do + 'class { "apache": mpm_module => prefork, }' + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('php5') } + it { should contain_package("php") } + it { should contain_file("php5.load").with( + :content => "LoadModule php5_module modules/libphp5.so\n" + ) } + end + end + describe "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + context "with mpm_module => prefork" do + let :pre_condition do + 'class { "apache": mpm_module => prefork, }' + end + it { should contain_class('apache::params') } + it { should contain_apache__mod('php5') } + it { should contain_package("lang/php5") } + it { should contain_file('php5.load') } + end + # FIXME: not sure about the following context + context 'with mpm_module => worker' do + let :pre_condition do + 'class { "apache": mpm_module => worker, }' + end + it 'should raise an error' do + expect { subject.should contain_apache__mod('php5') }.to raise_error Puppet::Error, /mpm_module => 'prefork'/ + end + end + end +end diff --git a/apache/spec/classes/mod/prefork_spec.rb b/apache/spec/classes/mod/prefork_spec.rb new file mode 100644 index 000000000..8eff78e4a --- /dev/null +++ b/apache/spec/classes/mod/prefork_spec.rb @@ -0,0 +1,99 @@ +describe 'apache::mod::prefork', :type => :class do + let :pre_condition do + 'class { "apache": mpm_module => false, }' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('prefork') } + it { should contain_file("/etc/apache2/mods-available/prefork.conf").with_ensure('file') } + it { should contain_file("/etc/apache2/mods-enabled/prefork.conf").with_ensure('link') } + + context "with Apache version < 2.4" do + let :params do + { + :apache_version => 2.2, + } + end + + it { should_not contain_file("/etc/apache2/mods-available/prefork.load") } + it { should_not contain_file("/etc/apache2/mods-enabled/prefork.load") } + + it { should contain_package("apache2-mpm-prefork") } + end + + context "with Apache version >= 2.4" do + let :params do + { + :apache_version => 2.4, + } + end + + it { should contain_file("/etc/apache2/mods-available/prefork.load").with({ + 'ensure' => 'file', + 'content' => "LoadModule mpm_prefork_module /usr/lib/apache2/modules/mod_mpm_prefork.so\n" + }) + } + it { should contain_file("/etc/apache2/mods-enabled/prefork.load").with_ensure('link') } + end + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('prefork') } + it { should contain_file("/etc/httpd/conf.d/prefork.conf").with_ensure('file') } + + context "with Apache version < 2.4" do + let :params do + { + :apache_version => 2.2, + } + end + + it { should contain_file_line("/etc/sysconfig/httpd prefork enable").with({ + 'require' => 'Package[httpd]', + }) + } + end + + context "with Apache version >= 2.4" do + let :params do + { + :apache_version => 2.4, + } + end + + it { should_not contain_apache__mod('event') } + + it { should contain_file("/etc/httpd/conf.d/prefork.load").with({ + 'ensure' => 'file', + 'content' => "LoadModule mpm_prefork_module modules/mod_mpm_prefork.so\n", + }) + } + end + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('prefork') } + it { should contain_file("/usr/local/etc/apache22/Modules/prefork.conf").with_ensure('file') } + end +end diff --git a/apache/spec/classes/mod/proxy_html_spec.rb b/apache/spec/classes/mod/proxy_html_spec.rb new file mode 100644 index 000000000..90be60b0f --- /dev/null +++ b/apache/spec/classes/mod/proxy_html_spec.rb @@ -0,0 +1,45 @@ +describe 'apache::mod::proxy_html', :type => :class do + let :pre_condition do + [ + 'include apache', + 'include apache::mod::proxy', + 'include apache::mod::proxy_http', + ] + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('proxy_html') } + it { should contain_package("libapache2-mod-proxy-html") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('proxy_html') } + it { should contain_package("mod_proxy_html") } + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('proxy_html') } + it { should contain_package("www/mod_proxy_html") } + end +end diff --git a/apache/spec/classes/mod/python_spec.rb b/apache/spec/classes/mod/python_spec.rb new file mode 100644 index 000000000..9042d0f1b --- /dev/null +++ b/apache/spec/classes/mod/python_spec.rb @@ -0,0 +1,41 @@ +describe 'apache::mod::python', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod("python") } + it { should contain_package("libapache2-mod-python") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod("python") } + it { should contain_package("mod_python") } + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod("python") } + it { should contain_package("www/mod_python3") } + end +end diff --git a/apache/spec/classes/mod/rpaf_spec.rb b/apache/spec/classes/mod/rpaf_spec.rb new file mode 100644 index 000000000..d9c9015ab --- /dev/null +++ b/apache/spec/classes/mod/rpaf_spec.rb @@ -0,0 +1,77 @@ +describe 'apache::mod::rpaf', :type => :class do + let :pre_condition do + [ + 'include apache', + ] + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('rpaf') } + it { should contain_package("libapache2-mod-rpaf") } + it { should contain_file('rpaf.conf').with({ + 'path' => '/etc/apache2/mods-available/rpaf.conf', + }) } + it { should contain_file('rpaf.conf').with_content(/^RPAFenable On$/) } + + describe "with sethostname => true" do + let :params do + { :sethostname => 'true' } + end + it { should contain_file('rpaf.conf').with_content(/^RPAFsethostname On$/) } + end + describe "with proxy_ips => [ 10.42.17.8, 10.42.18.99 ]" do + let :params do + { :proxy_ips => [ '10.42.17.8', '10.42.18.99' ] } + end + it { should contain_file('rpaf.conf').with_content(/^RPAFproxy_ips 10.42.17.8 10.42.18.99$/) } + end + describe "with header => X-Real-IP" do + let :params do + { :header => 'X-Real-IP' } + end + it { should contain_file('rpaf.conf').with_content(/^RPAFheader X-Real-IP$/) } + end + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('rpaf') } + it { should contain_package("www/mod_rpaf2") } + it { should contain_file('rpaf.conf').with({ + 'path' => '/usr/local/etc/apache22/Modules/rpaf.conf', + }) } + it { should contain_file('rpaf.conf').with_content(/^RPAFenable On$/) } + + describe "with sethostname => true" do + let :params do + { :sethostname => 'true' } + end + it { should contain_file('rpaf.conf').with_content(/^RPAFsethostname On$/) } + end + describe "with proxy_ips => [ 10.42.17.8, 10.42.18.99 ]" do + let :params do + { :proxy_ips => [ '10.42.17.8', '10.42.18.99' ] } + end + it { should contain_file('rpaf.conf').with_content(/^RPAFproxy_ips 10.42.17.8 10.42.18.99$/) } + end + describe "with header => X-Real-IP" do + let :params do + { :header => 'X-Real-IP' } + end + it { should contain_file('rpaf.conf').with_content(/^RPAFheader X-Real-IP$/) } + end + end +end diff --git a/apache/spec/classes/mod/ssl_spec.rb b/apache/spec/classes/mod/ssl_spec.rb new file mode 100644 index 000000000..45005a191 --- /dev/null +++ b/apache/spec/classes/mod/ssl_spec.rb @@ -0,0 +1,53 @@ +describe 'apache::mod::ssl', :type => :class do + let :pre_condition do + 'include apache' + end + context 'on an unsupported OS' do + let :facts do + { + :osfamily => 'Magic', + :operatingsystemrelease => '0', + :concat_basedir => '/dne', + } + end + it { expect { subject }.to raise_error(Puppet::Error, /Unsupported osfamily:/) } + end + + context 'on a RedHat OS' do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class('apache::params') } + it { should contain_apache__mod('ssl') } + it { should contain_package('mod_ssl') } + end + + context 'on a Debian OS' do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class('apache::params') } + it { should contain_apache__mod('ssl') } + it { should_not contain_package('libapache2-mod-ssl') } + end + + context 'on a FreeBSD OS' do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class('apache::params') } + it { should contain_apache__mod('ssl') } + end +end diff --git a/apache/spec/classes/mod/status_spec.rb b/apache/spec/classes/mod/status_spec.rb new file mode 100644 index 000000000..0a0658879 --- /dev/null +++ b/apache/spec/classes/mod/status_spec.rb @@ -0,0 +1,166 @@ +require 'spec_helper' + +# Helper function for testing the contents of `status.conf` +def status_conf_spec(allow_from, extended_status) + it do + should contain_file("status.conf").with_content( + "\n"\ + " SetHandler server-status\n"\ + " Order deny,allow\n"\ + " Deny from all\n"\ + " Allow from #{Array(allow_from).join(' ')}\n"\ + "\n"\ + "ExtendedStatus #{extended_status}\n"\ + "\n"\ + "\n"\ + " # Show Proxy LoadBalancer status in mod_status\n"\ + " ProxyStatus On\n"\ + "\n" + ) + end +end + +describe 'apache::mod::status', :type => :class do + let :pre_condition do + 'include apache' + end + + context "on a Debian OS with default params" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + it { should contain_apache__mod("status") } + + status_conf_spec(["127.0.0.1", "::1"], "On") + + it { should contain_file("status.conf").with({ + :ensure => 'file', + :path => '/etc/apache2/mods-available/status.conf', + } ) } + + it { should contain_file("status.conf symlink").with({ + :ensure => 'link', + :path => '/etc/apache2/mods-enabled/status.conf', + } ) } + + end + + context "on a RedHat OS with default params" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + it { should contain_apache__mod("status") } + + status_conf_spec(["127.0.0.1", "::1"], "On") + + it { should contain_file("status.conf").with_path("/etc/httpd/conf.d/status.conf") } + + end + + context "with custom parameters $allow_from => ['10.10.10.10','11.11.11.11'], $extended_status => 'Off'" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + let :params do + { + :allow_from => ['10.10.10.10','11.11.11.11'], + :extended_status => 'Off', + } + end + + status_conf_spec(["10.10.10.10", "11.11.11.11"], "Off") + + end + + context "with valid parameter type $allow_from => ['10.10.10.10']" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + let :params do + { :allow_from => ['10.10.10.10'] } + end + it 'should expect to succeed array validation' do + expect { + should contain_file("status.conf") + }.not_to raise_error() + end + end + + context "with invalid parameter type $allow_from => '10.10.10.10'" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + let :params do + { :allow_from => '10.10.10.10' } + end + it 'should expect to fail array validation' do + expect { + should contain_file("status.conf") + }.to raise_error(Puppet::Error) + end + end + + # Only On or Off are valid options + ['On', 'Off'].each do |valid_param| + context "with valid value $extended_status => '#{valid_param}'" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + let :params do + { :extended_status => valid_param } + end + it 'should expect to succeed regular expression validation' do + expect { + should contain_file("status.conf") + }.not_to raise_error() + end + end + end + + ['Yes', 'No'].each do |invalid_param| + context "with invalid value $extended_status => '#{invalid_param}'" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + let :params do + { :extended_status => invalid_param } + end + it 'should expect to fail regular expression validation' do + expect { + should contain_file("status.conf") + }.to raise_error(Puppet::Error) + end + end + end + +end diff --git a/apache/spec/classes/mod/suphp_spec.rb b/apache/spec/classes/mod/suphp_spec.rb new file mode 100644 index 000000000..382314d9a --- /dev/null +++ b/apache/spec/classes/mod/suphp_spec.rb @@ -0,0 +1,27 @@ +describe 'apache::mod::suphp', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_package("libapache2-mod-suphp") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_package("mod_suphp") } + end +end diff --git a/apache/spec/classes/mod/worker_spec.rb b/apache/spec/classes/mod/worker_spec.rb new file mode 100644 index 000000000..504018e68 --- /dev/null +++ b/apache/spec/classes/mod/worker_spec.rb @@ -0,0 +1,99 @@ +describe 'apache::mod::worker', :type => :class do + let :pre_condition do + 'class { "apache": mpm_module => false, }' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('worker') } + it { should contain_file("/etc/apache2/mods-available/worker.conf").with_ensure('file') } + it { should contain_file("/etc/apache2/mods-enabled/worker.conf").with_ensure('link') } + + context "with Apache version < 2.4" do + let :params do + { + :apache_version => 2.2, + } + end + + it { should_not contain_file("/etc/apache2/mods-available/worker.load") } + it { should_not contain_file("/etc/apache2/mods-enabled/worker.load") } + + it { should contain_package("apache2-mpm-worker") } + end + + context "with Apache version >= 2.4" do + let :params do + { + :apache_version => 2.4, + } + end + + it { should contain_file("/etc/apache2/mods-available/worker.load").with({ + 'ensure' => 'file', + 'content' => "LoadModule mpm_worker_module /usr/lib/apache2/modules/mod_mpm_worker.so\n" + }) + } + it { should contain_file("/etc/apache2/mods-enabled/worker.load").with_ensure('link') } + end + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('worker') } + it { should contain_file("/etc/httpd/conf.d/worker.conf").with_ensure('file') } + + context "with Apache version < 2.4" do + let :params do + { + :apache_version => 2.2, + } + end + + it { should contain_file_line("/etc/sysconfig/httpd worker enable").with({ + 'require' => 'Package[httpd]', + }) + } + end + + context "with Apache version >= 2.4" do + let :params do + { + :apache_version => 2.4, + } + end + + it { should_not contain_apache__mod('event') } + + it { should contain_file("/etc/httpd/conf.d/worker.load").with({ + 'ensure' => 'file', + 'content' => "LoadModule mpm_worker_module modules/mod_mpm_worker.so\n", + }) + } + end + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should_not contain_apache__mod('worker') } + it { should contain_file("/usr/local/etc/apache22/Modules/worker.conf").with_ensure('file') } + end +end diff --git a/apache/spec/classes/mod/wsgi_spec.rb b/apache/spec/classes/mod/wsgi_spec.rb new file mode 100644 index 000000000..44917cb88 --- /dev/null +++ b/apache/spec/classes/mod/wsgi_spec.rb @@ -0,0 +1,54 @@ +describe 'apache::mod::wsgi', :type => :class do + let :pre_condition do + 'include apache' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('wsgi') } + it { should contain_package("libapache2-mod-wsgi") } + end + context "on a RedHat OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('wsgi') } + it { should contain_package("mod_wsgi") } + + describe "with custom WSGISocketPrefix" do + let :params do + { :wsgi_socket_prefix => 'run/wsgi' } + end + it {should contain_file('wsgi.conf').with_content(/^ WSGISocketPrefix run\/wsgi$/)} + end + describe "with custom WSGIPythonHome" do + let :params do + { :wsgi_python_home => '/path/to/virtenv' } + end + it {should contain_file('wsgi.conf').with_content(/^ WSGIPythonHome "\/path\/to\/virtenv"$/)} + end + end + context "on a FreeBSD OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_class("apache::params") } + it { should contain_apache__mod('wsgi') } + it { should contain_package("www/mod_wsgi") } + end +end diff --git a/apache/spec/classes/params_spec.rb b/apache/spec/classes/params_spec.rb new file mode 100644 index 000000000..de1108af0 --- /dev/null +++ b/apache/spec/classes/params_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'apache::params', :type => :class do + context "On a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_apache__params } + + # There are 4 resources in this class currently + # there should not be any more resources because it is a params class + # The resources are class[apache::version], class[apache::params], class[main], class[settings], stage[main] + it "Should not contain any resources" do + subject.resources.size.should == 5 + end + end +end diff --git a/apache/spec/classes/service_spec.rb b/apache/spec/classes/service_spec.rb new file mode 100644 index 000000000..accc54946 --- /dev/null +++ b/apache/spec/classes/service_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe 'apache::service', :type => :class do + let :pre_condition do + 'include apache::params' + end + context "on a Debian OS" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + it { should contain_service("httpd").with( + 'name' => 'apache2', + 'ensure' => 'running', + 'enable' => 'true' + ) + } + + context "with $service_name => 'foo'" do + let (:params) {{ :service_name => 'foo' }} + it { should contain_service("httpd").with( + 'name' => 'foo' + ) + } + end + + context "with $service_enable => true" do + let (:params) {{ :service_enable => true }} + it { should contain_service("httpd").with( + 'name' => 'apache2', + 'ensure' => 'running', + 'enable' => 'true' + ) + } + end + + context "with $service_enable => false" do + let (:params) {{ :service_enable => false }} + it { should contain_service("httpd").with( + 'name' => 'apache2', + 'ensure' => 'running', + 'enable' => 'false' + ) + } + end + + context "$service_enable must be a bool" do + let (:params) {{ :service_enable => 'not-a-boolean' }} + + it 'should fail' do + expect { subject }.to raise_error(Puppet::Error, /is not a boolean/) + end + end + + context "with $service_ensure => 'running'" do + let (:params) {{ :service_ensure => 'running', }} + it { should contain_service("httpd").with( + 'ensure' => 'running', + 'enable' => 'true' + ) + } + end + + context "with $service_ensure => 'stopped'" do + let (:params) {{ :service_ensure => 'stopped', }} + it { should contain_service("httpd").with( + 'ensure' => 'stopped', + 'enable' => 'true' + ) + } + end + end + + + context "on a RedHat 5 OS" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '5', + :concat_basedir => '/dne', + } + end + it { should contain_service("httpd").with( + 'name' => 'httpd', + 'ensure' => 'running', + 'enable' => 'true' + ) + } + end + + context "on a FreeBSD 5 OS" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + it { should contain_service("httpd").with( + 'name' => 'apache22', + 'ensure' => 'running', + 'enable' => 'true' + ) + } + end +end diff --git a/apache/spec/defines/mod_spec.rb b/apache/spec/defines/mod_spec.rb new file mode 100644 index 000000000..bbc5f0bdc --- /dev/null +++ b/apache/spec/defines/mod_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe 'apache::mod', :type => :define do + let :pre_condition do + 'include apache' + end + context "on a RedHat osfamily" do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + describe "for non-special modules" do + let :title do + 'spec_m' + end + it { should contain_class("apache::params") } + it "should manage the module load file" do + should contain_file('spec_m.load').with({ + :path => '/etc/httpd/conf.d/spec_m.load', + :content => "LoadModule spec_m_module modules/mod_spec_m.so\n", + :owner => 'root', + :group => 'root', + :mode => '0644', + } ) + end + end + + describe "with shibboleth module and package param passed" do + # name/title for the apache::mod define + let :title do + 'xsendfile' + end + # parameters + let(:params) { {:package => 'mod_xsendfile'} } + + it { should contain_class("apache::params") } + it { should contain_package('mod_xsendfile') } + end + end + + context "on a Debian osfamily" do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + + describe "for non-special modules" do + let :title do + 'spec_m' + end + it { should contain_class("apache::params") } + it "should manage the module load file" do + should contain_file('spec_m.load').with({ + :path => '/etc/apache2/mods-available/spec_m.load', + :content => "LoadModule spec_m_module /usr/lib/apache2/modules/mod_spec_m.so\n", + :owner => 'root', + :group => 'root', + :mode => '0644', + } ) + end + it "should link the module load file" do + should contain_file('spec_m.load symlink').with({ + :path => '/etc/apache2/mods-enabled/spec_m.load', + :target => '/etc/apache2/mods-available/spec_m.load', + :owner => 'root', + :group => 'root', + :mode => '0644', + } ) + end + end + end + + context "on a FreeBSD osfamily" do + let :facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + + describe "for non-special modules" do + let :title do + 'spec_m' + end + it { should contain_class("apache::params") } + it "should manage the module load file" do + should contain_file('spec_m.load').with({ + :path => '/usr/local/etc/apache22/Modules/spec_m.load', + :content => "LoadModule spec_m_module /usr/local/libexec/apache22/mod_spec_m.so\n", + :owner => 'root', + :group => 'wheel', + :mode => '0644', + } ) + end + end + end +end diff --git a/apache/spec/defines/vhost_spec.rb b/apache/spec/defines/vhost_spec.rb new file mode 100644 index 000000000..46e6fd395 --- /dev/null +++ b/apache/spec/defines/vhost_spec.rb @@ -0,0 +1,1378 @@ +require 'spec_helper' + +describe 'apache::vhost', :type => :define do + let :pre_condition do + 'class { "apache": default_vhost => false, }' + end + let :title do + 'rspec.example.com' + end + let :default_params do + { + :docroot => '/rspec/docroot', + :port => '84', + } + end + describe 'os-dependent items' do + context "on RedHat based systems" do + let :default_facts do + { + :osfamily => 'RedHat', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + let :params do default_params end + let :facts do default_facts end + it { should contain_class("apache") } + it { should contain_class("apache::params") } + end + context "on Debian based systems" do + let :default_facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + let :params do default_params end + let :facts do default_facts end + it { should contain_class("apache") } + it { should contain_class("apache::params") } + it { should contain_file("25-rspec.example.com.conf").with( + :ensure => 'present', + :path => '/etc/apache2/sites-available/25-rspec.example.com.conf' + ) } + it { should contain_file("25-rspec.example.com.conf symlink").with( + :ensure => 'link', + :path => '/etc/apache2/sites-enabled/25-rspec.example.com.conf', + :target => '/etc/apache2/sites-available/25-rspec.example.com.conf' + ) } + end + context "on FreeBSD systems" do + let :default_facts do + { + :osfamily => 'FreeBSD', + :operatingsystemrelease => '9', + :concat_basedir => '/dne', + } + end + let :params do default_params end + let :facts do default_facts end + it { should contain_class("apache") } + it { should contain_class("apache::params") } + it { should contain_file("25-rspec.example.com.conf").with( + :ensure => 'present', + :path => '/usr/local/etc/apache22/Vhosts/25-rspec.example.com.conf' + ) } + end + end + describe 'os-independent items' do + let :facts do + { + :osfamily => 'Debian', + :operatingsystemrelease => '6', + :concat_basedir => '/dne', + } + end + describe 'basic assumptions' do + let :params do default_params end + it { should contain_class("apache") } + it { should contain_class("apache::params") } + it { should contain_apache__listen(params[:port]) } + it { should contain_apache__namevirtualhost("*:#{params[:port]}") } + end + + # All match and notmatch should be a list of regexs and exact match strings + context ".conf content" do + [ + { + :title => 'should contain docroot', + :attr => 'docroot', + :value => '/not/default', + :match => [/^ DocumentRoot "\/not\/default"$/,/ /], + }, + { + :title => 'should set a port', + :attr => 'port', + :value => '8080', + :match => [/^$/], + }, + { + :title => 'should set an ip', + :attr => 'ip', + :value => '10.0.0.1', + :match => [/^$/], + }, + { + :title => 'should set a serveradmin', + :attr => 'serveradmin', + :value => 'test@test.com', + :match => [/^ ServerAdmin test@test.com$/], + }, + { + :title => 'should enable ssl', + :attr => 'ssl', + :value => true, + :match => [/^ SSLEngine on$/], + }, + { + :title => 'should set a servername', + :attr => 'servername', + :value => 'param.test', + :match => [/^ ServerName param.test$/], + }, + { + :title => 'should accept server aliases', + :attr => 'serveraliases', + :value => ['one.com','two.com'], + :match => [ + /^ ServerAlias one\.com$/, + /^ ServerAlias two\.com$/ + ], + }, + { + :title => 'should accept setenv', + :attr => 'setenv', + :value => ['TEST1 one','TEST2 two'], + :match => [ + /^ SetEnv TEST1 one$/, + /^ SetEnv TEST2 two$/ + ], + }, + { + :title => 'should accept setenvif', + :attr => 'setenvif', + ## These are bugged in rspec-puppet; the $1 is droped + #:value => ['Host "^([^\.]*)\.website\.com$" CLIENT_NAME=$1'], + #:match => [' SetEnvIf Host "^([^\.]*)\.website\.com$" CLIENT_NAME=$1'], + :value => ['Host "^test\.com$" VHOST_ACCESS=test'], + :match => [/^ SetEnvIf Host "\^test\\.com\$" VHOST_ACCESS=test$/], + }, + { + :title => 'should accept options', + :attr => 'options', + :value => ['Fake','Options'], + :match => [/^ Options Fake Options$/], + }, + { + :title => 'should accept overrides', + :attr => 'override', + :value => ['Fake', 'Override'], + :match => [/^ AllowOverride Fake Override$/], + }, + { + :title => 'should accept logroot', + :attr => 'logroot', + :value => '/fake/log', + :match => [/CustomLog "\/fake\/log\//,/ErrorLog "\/fake\/log\//], + }, + { + :title => 'should accept log_level', + :attr => 'log_level', + :value => 'info', + :match => [/LogLevel info/], + }, + { + :title => 'should accept pipe destination for access log', + :attr => 'access_log_pipe', + :value => '| /bin/fake/logging', + :match => [/CustomLog "| \/bin\/fake\/logging" combined$/], + }, + { + :title => 'should accept pipe destination for error log', + :attr => 'error_log_pipe', + :value => '| /bin/fake/logging', + :match => [/ErrorLog "| \/bin\/fake\/logging" combined$/], + }, + { + :title => 'should accept syslog destination for access log', + :attr => 'access_log_syslog', + :value => 'syslog:local1', + :match => [/CustomLog "syslog:local1" combined$/], + }, + { + :title => 'should accept syslog destination for error log', + :attr => 'error_log_syslog', + :value => 'syslog', + :match => [/ErrorLog "syslog"$/], + }, + { + :title => 'should accept custom format for access logs', + :attr => 'access_log_format', + :value => '%h %{X-Forwarded-For}i %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\" \"Host: %{Host}i\" %T %D', + :match => [/CustomLog "\/var\/log\/.+_access\.log" "%h %\{X-Forwarded-For\}i %l %u %t \\"%r\\" %s %b \\"%\{Referer\}i\\" \\"%\{User-agent\}i\\" \\"Host: %\{Host\}i\\" %T %D"$/], + }, + { + :title => 'should contain access logs', + :attr => 'access_log', + :value => true, + :match => [/CustomLog "\/var\/log\/.+_access\.log" combined$/], + }, + { + :title => 'should not contain access logs', + :attr => 'access_log', + :value => false, + :notmatch => [/CustomLog "\/var\/log\/.+_access\.log" combined$/], + }, + { + :title => 'should contain error logs', + :attr => 'error_log', + :value => true, + :match => [/ErrorLog.+$/], + }, + { + :title => 'should not contain error logs', + :attr => 'error_log', + :value => false, + :notmatch => [/ErrorLog.+$/], + }, + { + :title => 'should set ErrorDocument 503', + :attr => 'error_documents', + :value => [ { 'error_code' => '503', 'document' => '"Go away, the backend is broken."'}], + :match => [/^ ErrorDocument 503 "Go away, the backend is broken."$/], + }, + { + :title => 'should set ErrorDocuments 503 407', + :attr => 'error_documents', + :value => [ + { 'error_code' => '503', 'document' => '/service-unavail'}, + { 'error_code' => '407', 'document' => 'https://example.com/proxy/login'}, + ], + :match => [ + /^ ErrorDocument 503 \/service-unavail$/, + /^ ErrorDocument 407 https:\/\/example\.com\/proxy\/login$/, + ], + }, + { + :title => 'should set ErrorDocument 503 in directory', + :attr => 'directories', + :value => { 'path' => '/srv/www', 'error_documents' => [{ 'error_code' => '503', 'document' => '"Go away, the backend is broken."'}] }, + :match => [/^ ErrorDocument 503 "Go away, the backend is broken."$/], + }, + { + :title => 'should set ErrorDocuments 503 407 in directory', + :attr => 'directories', + :value => { 'path' => '/srv/www', 'error_documents' => + [ + { 'error_code' => '503', 'document' => '/service-unavail'}, + { 'error_code' => '407', 'document' => 'https://example.com/proxy/login'}, + ]}, + :match => [ + /^ ErrorDocument 503 \/service-unavail$/, + /^ ErrorDocument 407 https:\/\/example\.com\/proxy\/login$/, + ], + }, + { + :title => 'should accept a scriptalias', + :attr => 'scriptalias', + :value => '/usr/scripts', + :match => [ + /^ ScriptAlias \/cgi-bin\/ "\/usr\/scripts"$/, + ], + }, + { + :title => 'should accept a single scriptaliases', + :attr => 'scriptaliases', + :value => { 'alias' => '/blah/', 'path' => '/usr/scripts' }, + :match => [ + /^ ScriptAlias \/blah\/ "\/usr\/scripts"$/, + ], + :nomatch => [/ScriptAlias \/cgi\-bin\//], + }, + { + :title => 'should accept multiple scriptaliases', + :attr => 'scriptaliases', + :value => [ { 'alias' => '/blah', 'path' => '/usr/scripts' }, { 'alias' => '/blah2', 'path' => '/usr/scripts' } ], + :match => [ + /^ ScriptAlias \/blah "\/usr\/scripts"$/, + /^ ScriptAlias \/blah2 "\/usr\/scripts"$/, + ], + :nomatch => [/ScriptAlias \/cgi\-bin\//], + }, + { + :title => 'should accept multiple scriptaliases with and without trailing slashes', + :attr => 'scriptaliases', + :value => [ { 'alias' => '/blah', 'path' => '/usr/scripts' }, { 'alias' => '/blah2/', 'path' => '/usr/scripts2/' } ], + :match => [ + /^ ScriptAlias \/blah "\/usr\/scripts"$/, + /^ ScriptAlias \/blah2\/ "\/usr\/scripts2\/"$/, + ], + :nomatch => [/ScriptAlias \/cgi\-bin\//], + }, + { + :title => 'should accept a ScriptAliasMatch directive', + :attr => 'scriptaliases', + ## XXX As mentioned above, rspec-puppet drops constructs like $1. + ## Thus, these tests don't work as they should. As a workaround we + ## use FOO instead of $1 here. + :value => [ { 'aliasmatch' => '^/cgi-bin(.*)', 'path' => '/usr/local/apache/cgi-binFOO' } ], + :match => [ + /^ ScriptAliasMatch \^\/cgi-bin\(\.\*\) "\/usr\/local\/apache\/cgi-binFOO"$/ + ], + }, + { + :title => 'should accept multiple ScriptAliasMatch directives', + :attr => 'scriptaliases', + ## XXX As mentioned above, rspec-puppet drops constructs like $1. + ## Thus, these tests don't work as they should. As a workaround we + ## use FOO instead of $1 here. + :value => [ + { 'aliasmatch' => '^/cgi-bin(.*)', 'path' => '/usr/local/apache/cgi-binFOO' }, + { 'aliasmatch' => '"(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))|git-(upload|receive)-pack))"', 'path' => '/var/www/bin/gitolite-suexec-wrapper/FOO' }, + ], + :match => [ + /^ ScriptAliasMatch \^\/cgi-bin\(\.\*\) "\/usr\/local\/apache\/cgi-binFOO"$/, + /^ ScriptAliasMatch "\(\?x\)\^\/git\/\(\.\*\/\(HEAD\|info\/refs\|objects\/\(info\/\[\^\/\]\+\|\[0-9a-f\]\{2\}\/\[0-9a-f\]\{38\}\|pack\/pack-\[0-9a-f\]\{40\}\\\.\(pack\|idx\)\)\|git-\(upload\|receive\)-pack\)\)" "\/var\/www\/bin\/gitolite-suexec-wrapper\/FOO"$/, + ], + }, + { + :title => 'should accept mixed ScriptAlias and ScriptAliasMatch directives', + :attr => 'scriptaliases', + ## XXX As mentioned above, rspec-puppet drops constructs like $1. + ## Thus, these tests don't work as they should. As a workaround we + ## use FOO instead of $1 here. + :value => [ + { 'aliasmatch' => '"(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))|git-(upload|receive)-pack))"', 'path' => '/var/www/bin/gitolite-suexec-wrapper/FOO' }, + { 'alias' => '/git', 'path' => '/var/www/gitweb/index.cgi' }, + { 'aliasmatch' => '^/cgi-bin(.*)', 'path' => '/usr/local/apache/cgi-binFOO' }, + { 'alias' => '/trac', 'path' => '/etc/apache2/trac.fcgi' }, + ], + :match => [ + /^ ScriptAliasMatch "\(\?x\)\^\/git\/\(\.\*\/\(HEAD\|info\/refs\|objects\/\(info\/\[\^\/\]\+\|\[0-9a-f\]\{2\}\/\[0-9a-f\]\{38\}\|pack\/pack-\[0-9a-f\]\{40\}\\\.\(pack\|idx\)\)\|git-\(upload\|receive\)-pack\)\)" "\/var\/www\/bin\/gitolite-suexec-wrapper\/FOO"$/, + /^ ScriptAlias \/git "\/var\/www\/gitweb\/index\.cgi"$/, + /^ ScriptAliasMatch \^\/cgi-bin\(\.\*\) "\/usr\/local\/apache\/cgi-binFOO"$/, + /^ ScriptAlias \/trac "\/etc\/apache2\/trac.fcgi"$/, + ], + }, + { + :title => 'should accept proxy destinations', + :attr => 'proxy_dest', + :value => 'http://fake.com', + :match => [ + /^ ProxyPass \/ http:\/\/fake.com\/$/, + /^ $/, + /^ ProxyPassReverse http:\/\/fake.com\/$/, + /^ <\/Location>$/, + ], + :notmatch => [/ProxyPass .+!$/], + }, + { + :title => 'should accept proxy_pass hash', + :attr => 'proxy_pass', + :value => { 'path' => '/path-a', 'url' => 'http://fake.com/a' }, + :match => [ + /^ ProxyPass \/path-a http:\/\/fake.com\/a$/, + /^ $/, + /^ ProxyPassReverse http:\/\/fake.com\/a$/, + /^ <\/Location>$/, + + ], + :notmatch => [/ProxyPass .+!$/], + }, + { + :title => 'should accept proxy_pass array of hash', + :attr => 'proxy_pass', + :value => [ + { 'path' => '/path-a/', 'url' => 'http://fake.com/a/' }, + { 'path' => '/path-b', 'url' => 'http://fake.com/b' }, + ], + :match => [ + /^ ProxyPass \/path-a\/ http:\/\/fake.com\/a\/$/, + /^ $/, + /^ ProxyPassReverse http:\/\/fake.com\/a\/$/, + /^ <\/Location>$/, + /^ ProxyPass \/path-b http:\/\/fake.com\/b$/, + /^ $/, + /^ ProxyPassReverse http:\/\/fake.com\/b$/, + /^ <\/Location>$/, + ], + :notmatch => [/ProxyPass .+!$/], + }, + { + :title => 'should enable rack', + :attr => 'rack_base_uris', + :value => ['/rack1','/rack2'], + :match => [ + /^ RackBaseURI \/rack1$/, + /^ RackBaseURI \/rack2$/, + ], + }, + { + :title => 'should accept headers', + :attr => 'headers', + :value => ['add something', 'merge something_else'], + :match => [ + /^ Header add something$/, + /^ Header merge something_else$/, + ], + }, + { + :title => 'should accept request headers', + :attr => 'request_headers', + :value => ['append something', 'unset something_else'], + :match => [ + /^ RequestHeader append something$/, + /^ RequestHeader unset something_else$/, + ], + }, + { + :title => 'should accept rewrite rules', + :attr => 'rewrite_rule', + :value => 'not a real rule', + :match => [/^ RewriteRule not a real rule$/], + }, + { + :title => 'should accept rewrite rules', + :attr => 'rewrites', + :value => [{'rewrite_rule' => ['not a real rule']}], + :match => [/^ RewriteRule not a real rule$/], + }, + { + :title => 'should accept rewrite comment', + :attr => 'rewrites', + :value => [{'comment' => 'rewrite comment', 'rewrite_rule' => ['not a real rule']}], + :match => [/^ #rewrite comment/], + }, + { + :title => 'should accept rewrite conditions', + :attr => 'rewrites', + :value => [{'comment' => 'redirect IE', 'rewrite_cond' => ['%{HTTP_USER_AGENT} ^MSIE'], 'rewrite_rule' => ['^index\.html$ welcome.html'],}], + :match => [ + /^ #redirect IE$/, + /^ RewriteCond %{HTTP_USER_AGENT} \^MSIE$/, + /^ RewriteRule \^index\\\.html\$ welcome.html$/, + ], + }, + { + :title => 'should accept multiple rewrites', + :attr => 'rewrites', + :value => [ + {'rewrite_rule' => ['not a real rule']}, + {'rewrite_rule' => ['not a real rule two']}, + ], + :match => [ + /^ RewriteRule not a real rule$/, + /^ RewriteRule not a real rule two$/, + ], + }, + { + :title => 'should block scm', + :attr => 'block', + :value => 'scm', + :match => [/^ $/], + }, + { + :title => 'should accept a custom fragment', + :attr => 'custom_fragment', + :value => " Some custom fragment line\n That spans multiple lines", + :match => [ + /^ Some custom fragment line$/, + /^ That spans multiple lines$/, + /^<\/VirtualHost>$/, + ], + }, + { + :title => 'should accept an array of alias hashes', + :attr => 'aliases', + :value => [ { 'alias' => '/', 'path' => '/var/www'} ], + :match => [/^ Alias \/ "\/var\/www"$/], + }, + { + :title => 'should accept an alias hash', + :attr => 'aliases', + :value => { 'alias' => '/', 'path' => '/var/www'}, + :match => [/^ Alias \/ "\/var\/www"$/], + }, + { + :title => 'should accept multiple aliases', + :attr => 'aliases', + :value => [ + { 'alias' => '/', 'path' => '/var/www'}, + { 'alias' => '/cgi-bin', 'path' => '/var/www/cgi-bin'}, + { 'alias' => '/css', 'path' => '/opt/someapp/css'}, + ], + :match => [ + /^ Alias \/ "\/var\/www"$/, + /^ Alias \/cgi-bin "\/var\/www\/cgi-bin"$/, + /^ Alias \/css "\/opt\/someapp\/css"$/, + ], + }, + { + :title => 'should accept an aliasmatch hash', + :attr => 'aliases', + ## XXX As mentioned above, rspec-puppet drops the $1. Thus, these + # tests don't work. + #:value => { 'aliasmatch' => '^/image/(.*).gif', 'path' => '/files/gifs/$1.gif' }, + #:match => [/^ AliasMatch \^\/image\/\(\.\*\)\.gif \/files\/gifs\/\$1\.gif$/], + }, + { + :title => 'should accept a array of alias and aliasmatch hashes mixed', + :attr => 'aliases', + ## XXX As mentioned above, rspec-puppet drops the $1. Thus, these + # tests don't work. + #:value => [ + # { 'alias' => '/css', 'path' => '/files/css' }, + # { 'aliasmatch' => '^/image/(.*).gif', 'path' => '/files/gifs/$1.gif' }, + # { 'aliasmatch' => '^/image/(.*).jpg', 'path' => '/files/jpgs/$1.jpg' }, + # { 'alias' => '/image', 'path' => '/files/images' }, + #], + #:match => [ + # /^ Alias \/css \/files\/css$/, + # /^ AliasMatch \^\/image\/\(.\*\)\.gif \/files\/gifs\/\$1\.gif$/, + # /^ AliasMatch \^\/image\/\(.\*\)\.jpg \/files\/jpgs\/\$1\.jpg$/, + # /^ Alias \/image \/files\/images$/ + #], + }, + { + :title => 'should accept multiple additional includes', + :attr => 'additional_includes', + :value => [ + '/tmp/proxy_group_a', + '/tmp/proxy_group_b', + '/tmp/proxy_group_c', + ], + :match => [ + /^ Include "\/tmp\/proxy_group_a"$/, + /^ Include "\/tmp\/proxy_group_b"$/, + /^ Include "\/tmp\/proxy_group_c"$/, + ], + }, + { + :title => 'should accept a suPHP_Engine', + :attr => 'suphp_engine', + :value => 'on', + :match => [/^ suPHP_Engine on$/], + }, + { + :title => 'should accept a php_admin_flags', + :attr => 'php_admin_flags', + :value => { 'engine' => 'on' }, + :match => [/^ php_admin_flag engine on$/], + }, + { + :title => 'should accept php_admin_values', + :attr => 'php_admin_values', + :value => { 'open_basedir' => '/srv/web/www.com/:/usr/share/pear/' }, + :match => [/^ php_admin_value open_basedir \/srv\/web\/www.com\/:\/usr\/share\/pear\/$/], + }, + { + :title => 'should accept php_admin_flags in directories', + :attr => 'directories', + :value => { + 'path' => '/srv/www', + 'php_admin_flags' => { 'php_engine' => 'on' } + }, + :match => [/^ php_admin_flag php_engine on$/], + }, + { + :title => 'should accept php_admin_values', + :attr => 'php_admin_values', + :value => { 'open_basedir' => '/srv/web/www.com/:/usr/share/pear/' }, + :match => [/^ php_admin_value open_basedir \/srv\/web\/www.com\/:\/usr\/share\/pear\/$/], + }, + { + :title => 'should accept php_admin_values in directories', + :attr => 'directories', + :value => { + 'path' => '/srv/www', + 'php_admin_values' => { 'open_basedir' => '/srv/web/www.com/:/usr/share/pear/' } + }, + :match => [/^ php_admin_value open_basedir \/srv\/web\/www.com\/:\/usr\/share\/pear\/$/], + }, + { + :title => 'should accept a wsgi script alias', + :attr => 'wsgi_script_aliases', + :value => { '/' => '/var/www/myapp.wsgi'}, + :match => [/^ WSGIScriptAlias \/ "\/var\/www\/myapp.wsgi"$/], + }, + { + :title => 'should accept multiple wsgi aliases', + :attr => 'wsgi_script_aliases', + :value => { + '/wiki' => '/usr/local/wsgi/scripts/mywiki.wsgi', + '/blog' => '/usr/local/wsgi/scripts/myblog.wsgi', + '/' => '/usr/local/wsgi/scripts/myapp.wsgi', + }, + :match => [ + /^ WSGIScriptAlias \/wiki "\/usr\/local\/wsgi\/scripts\/mywiki.wsgi"$/, + /^ WSGIScriptAlias \/blog "\/usr\/local\/wsgi\/scripts\/myblog.wsgi"$/, + /^ WSGIScriptAlias \/ "\/usr\/local\/wsgi\/scripts\/myapp.wsgi"$/, + ], + }, + { + :title => 'should accept a wsgi application group', + :attr => 'wsgi_application_group', + :value => '%{GLOBAL}', + :match => [/^ WSGIApplicationGroup %{GLOBAL}$/], + }, + { + :title => 'should contain environment variables', + :attr => 'access_log_env_var', + :value => 'admin', + :match => [/CustomLog "\/var\/log\/.+_access\.log" combined env=admin$/] + }, + { + :title => 'should contain virtual_docroot', + :attr => 'virtual_docroot', + :value => '/not/default', + :match => [ + /^ VirtualDocumentRoot "\/not\/default"$/, + ], + }, + { + :title => 'should accept multiple directories', + :attr => 'directories', + :value => [ + { 'path' => '/opt/app' }, + { 'path' => '/var/www' }, + { 'path' => '/rspec/docroot'} + ], + :match => [ + /^ $/, + /^ $/, + /^ $/, + ], + }, + ].each do |param| + describe "when #{param[:attr]} is #{param[:value]}" do + let :params do default_params.merge({ param[:attr].to_sym => param[:value] }) end + + it { should contain_file("25-#{title}.conf").with_mode('0644') } + if param[:match] + it "#{param[:title]}: matches" do + param[:match].each do |match| + should contain_file("25-#{title}.conf").with_content( match ) + end + end + end + if param[:notmatch] + it "#{param[:title]}: notmatches" do + param[:notmatch].each do |notmatch| + should_not contain_file("25-#{title}.conf").with_content( notmatch ) + end + end + end + end + end + end + + # Apache below 2.4 (Default Version). All match and notmatch should be a list of regexs and exact match strings + context ".conf content with $apache_version < 2.4" do + [ + { + :title => 'should accept a directory', + :attr => 'directories', + :value => { 'path' => '/opt/app' }, + :notmatch => [' '], + :match => [ + /^ $/, + /^ AllowOverride None$/, + /^ Order allow,deny$/, + /^ Allow from all$/, + /^ <\/Directory>$/, + ], + }, + { + :title => 'should accept directory directives hash', + :attr => 'directories', + :value => { + 'path' => '/opt/app', + 'headers' => 'Set X-Robots-Tag "noindex, noarchive, nosnippet"', + 'allow' => 'from rspec.org', + 'allow_override' => 'Lol', + 'deny' => 'from google.com', + 'options' => '-MultiViews', + 'order' => 'deny,yned', + 'passenger_enabled' => 'onf', + }, + :match => [ + /^ $/, + /^ Header Set X-Robots-Tag "noindex, noarchive, nosnippet"$/, + /^ Allow from rspec.org$/, + /^ AllowOverride Lol$/, + /^ Deny from google.com$/, + /^ Options -MultiViews$/, + /^ Order deny,yned$/, + /^ PassengerEnabled onf$/, + /^ <\/Directory>$/, + ], + }, + { + :title => 'should accept directory directives with arrays and hashes', + :attr => 'directories', + :value => [ + { + 'path' => '/opt/app1', + 'allow' => 'from rspec.org', + 'allow_override' => ['AuthConfig','Indexes'], + 'deny' => 'from google.com', + 'options' => ['-MultiViews','+MultiViews'], + 'order' => ['deny','yned'], + 'passenger_enabled' => 'onf', + }, + { + 'path' => '/opt/app2', + 'addhandlers' => { + 'handler' => 'cgi-script', + 'extensions' => '.cgi', + }, + }, + ], + :match => [ + /^ $/, + /^ Allow from rspec.org$/, + /^ AllowOverride AuthConfig Indexes$/, + /^ Deny from google.com$/, + /^ Options -MultiViews \+MultiViews$/, + /^ Order deny,yned$/, + /^ PassengerEnabled onf$/, + /^ <\/Directory>$/, + /^ $/, + /^ AllowOverride None$/, + /^ Order allow,deny$/, + /^ Allow from all$/, + /^ AddHandler cgi-script .cgi$/, + /^ <\/Directory>$/, + ], + }, + { + :title => 'should accept location for provider', + :attr => 'directories', + :value => { + 'path' => '/', + 'provider' => 'location', + }, + :notmatch => [' AllowOverride None'], + :match => [ + /^ $/, + /^ Order allow,deny$/, + /^ Allow from all$/, + /^ <\/Location>$/, + ], + }, + { + :title => 'should accept files for provider', + :attr => 'directories', + :value => { + 'path' => 'index.html', + 'provider' => 'files', + }, + :notmatch => [' AllowOverride None'], + :match => [ + /^ $/, + /^ Order allow,deny$/, + /^ Allow from all$/, + /^ <\/Files>$/, + ], + }, + { + :title => 'should accept files match for provider', + :attr => 'directories', + :value => { + 'path' => 'index.html', + 'provider' => 'filesmatch', + }, + :notmatch => [' AllowOverride None'], + :match => [ + /^ $/, + /^ Order allow,deny$/, + /^ Allow from all$/, + /^ <\/FilesMatch>$/, + ], + }, + ].each do |param| + describe "when #{param[:attr]} is #{param[:value]}" do + let :params do default_params.merge({ + param[:attr].to_sym => param[:value], + :apache_version => 2.2, + }) end + + it { should contain_file("25-#{title}.conf").with_mode('0644') } + if param[:match] + it "#{param[:title]}: matches" do + param[:match].each do |match| + should contain_file("25-#{title}.conf").with_content( match ) + end + end + end + if param[:notmatch] + it "#{param[:title]}: notmatches" do + param[:notmatch].each do |notmatch| + should_not contain_file("25-#{title}.conf").with_content( notmatch ) + end + end + end + end + end + end + + # Apache equals or above 2.4. All match and notmatch should be a list of regexs and exact match strings + context ".conf content with $apache_version >= 2.4" do + [ + { + :title => 'should accept a directory', + :attr => 'directories', + :value => { 'path' => '/opt/app' }, + :notmatch => [' '], + :match => [ + /^ $/, + /^ AllowOverride None$/, + /^ Require all granted$/, + /^ <\/Directory>$/, + ], + }, + { + :title => 'should accept directory directives hash', + :attr => 'directories', + :value => { + 'path' => '/opt/app', + 'headers' => 'Set X-Robots-Tag "noindex, noarchive, nosnippet"', + 'allow_override' => 'Lol', + 'options' => '-MultiViews', + 'require' => 'something denied', + 'passenger_enabled' => 'onf', + }, + :match => [ + /^ $/, + /^ Header Set X-Robots-Tag "noindex, noarchive, nosnippet"$/, + /^ AllowOverride Lol$/, + /^ Options -MultiViews$/, + /^ Require something denied$/, + /^ PassengerEnabled onf$/, + /^ <\/Directory>$/, + ], + }, + { + :title => 'should accept directory directives with arrays and hashes', + :attr => 'directories', + :value => [ + { + 'path' => '/opt/app1', + 'allow_override' => ['AuthConfig','Indexes'], + 'options' => ['-MultiViews','+MultiViews'], + 'require' => ['host','example.org'], + 'passenger_enabled' => 'onf', + }, + { + 'path' => '/opt/app2', + 'addhandlers' => { + 'handler' => 'cgi-script', + 'extensions' => '.cgi', + }, + }, + ], + :match => [ + /^ $/, + /^ AllowOverride AuthConfig Indexes$/, + /^ Options -MultiViews \+MultiViews$/, + /^ Require host example.org$/, + /^ PassengerEnabled onf$/, + /^ <\/Directory>$/, + /^ $/, + /^ AllowOverride None$/, + /^ Require all granted$/, + /^ AddHandler cgi-script .cgi$/, + /^ <\/Directory>$/, + ], + }, + { + :title => 'should accept location for provider', + :attr => 'directories', + :value => { + 'path' => '/', + 'provider' => 'location', + }, + :notmatch => [' AllowOverride None'], + :match => [ + /^ $/, + /^ Require all granted$/, + /^ <\/Location>$/, + ], + }, + { + :title => 'should accept files for provider', + :attr => 'directories', + :value => { + 'path' => 'index.html', + 'provider' => 'files', + }, + :notmatch => [' AllowOverride None'], + :match => [ + /^ $/, + /^ Require all granted$/, + /^ <\/Files>$/, + ], + }, + { + :title => 'should accept files match for provider', + :attr => 'directories', + :value => { + 'path' => 'index.html', + 'provider' => 'filesmatch', + }, + :notmatch => [' AllowOverride None'], + :match => [ + /^ $/, + /^ Require all granted$/, + /^ <\/FilesMatch>$/, + ], + }, + ].each do |param| + describe "when #{param[:attr]} is #{param[:value]}" do + let :params do default_params.merge({ + param[:attr].to_sym => param[:value], + :apache_version => 2.4, + }) end + + it { should contain_file("25-#{title}.conf").with_mode('0644') } + if param[:match] + it "#{param[:title]}: matches" do + param[:match].each do |match| + should contain_file("25-#{title}.conf").with_content( match ) + end + end + end + if param[:notmatch] + it "#{param[:title]}: notmatches" do + param[:notmatch].each do |notmatch| + should_not contain_file("25-#{title}.conf").with_content( notmatch ) + end + end + end + end + end + end + + # All match and notmatch should be a list of regexs and exact match strings + context ".conf content with SSL" do + [ + { + :title => 'should accept setting SSLCertificateFile', + :attr => 'ssl_cert', + :value => '/path/to/cert.pem', + :match => [/^ SSLCertificateFile "\/path\/to\/cert\.pem"$/], + }, + { + :title => 'should accept setting SSLCertificateKeyFile', + :attr => 'ssl_key', + :value => '/path/to/cert.pem', + :match => [/^ SSLCertificateKeyFile "\/path\/to\/cert\.pem"$/], + }, + { + :title => 'should accept setting SSLCertificateChainFile', + :attr => 'ssl_chain', + :value => '/path/to/cert.pem', + :match => [/^ SSLCertificateChainFile "\/path\/to\/cert\.pem"$/], + }, + { + :title => 'should accept setting SSLCertificatePath', + :attr => 'ssl_certs_dir', + :value => '/path/to/certs', + :match => [/^ SSLCACertificatePath "\/path\/to\/certs"$/], + }, + { + :title => 'should accept setting SSLCertificateFile', + :attr => 'ssl_ca', + :value => '/path/to/ca.pem', + :match => [/^ SSLCACertificateFile "\/path\/to\/ca\.pem"$/], + }, + { + :title => 'should accept setting SSLRevocationPath', + :attr => 'ssl_crl_path', + :value => '/path/to/crl', + :match => [/^ SSLCARevocationPath "\/path\/to\/crl"$/], + }, + { + :title => 'should accept setting SSLRevocationFile', + :attr => 'ssl_crl', + :value => '/path/to/crl.pem', + :match => [/^ SSLCARevocationFile "\/path\/to\/crl\.pem"$/], + }, + { + :title => 'should accept setting SSLProxyEngine', + :attr => 'ssl_proxyengine', + :value => true, + :match => [/^ SSLProxyEngine On$/], + }, + { + :title => 'should accept setting SSLProtocol', + :attr => 'ssl_protocol', + :value => 'all -SSLv2', + :match => [/^ SSLProtocol all -SSLv2$/], + }, + { + :title => 'should accept setting SSLCipherSuite', + :attr => 'ssl_cipher', + :value => 'RC4-SHA:HIGH:!ADH:!SSLv2', + :match => [/^ SSLCipherSuite RC4-SHA:HIGH:!ADH:!SSLv2$/], + }, + { + :title => 'should accept setting SSLHonorCipherOrder', + :attr => 'ssl_honorcipherorder', + :value => 'On', + :match => [/^ SSLHonorCipherOrder On$/], + }, + { + :title => 'should accept setting SSLVerifyClient', + :attr => 'ssl_verify_client', + :value => 'optional', + :match => [/^ SSLVerifyClient optional$/], + }, + { + :title => 'should accept setting SSLVerifyDepth', + :attr => 'ssl_verify_depth', + :value => '1', + :match => [/^ SSLVerifyDepth 1$/], + }, + { + :title => 'should accept setting SSLOptions with a string', + :attr => 'ssl_options', + :value => '+ExportCertData', + :match => [/^ SSLOptions \+ExportCertData$/], + }, + { + :title => 'should accept setting SSLOptions with an array', + :attr => 'ssl_options', + :value => ['+StrictRequire','+ExportCertData'], + :match => [/^ SSLOptions \+StrictRequire \+ExportCertData/], + }, + { + :title => 'should accept setting SSLOptions with a string in directories', + :attr => 'directories', + :value => { 'path' => '/srv/www', 'ssl_options' => '+ExportCertData'}, + :match => [/^ SSLOptions \+ExportCertData$/], + }, + { + :title => 'should accept setting SSLOptions with an array in directories', + :attr => 'directories', + :value => { 'path' => '/srv/www', 'ssl_options' => ['-StdEnvVars','+ExportCertData']}, + :match => [/^ SSLOptions -StdEnvVars \+ExportCertData/], + }, + ].each do |param| + describe "when #{param[:attr]} is #{param[:value]} with SSL" do + let :params do + default_params.merge( { + param[:attr].to_sym => param[:value], + :ssl => true, + } ) + end + it { should contain_file("25-#{title}.conf").with_mode('0644') } + if param[:match] + it "#{param[:title]}: matches" do + param[:match].each do |match| + should contain_file("25-#{title}.conf").with_content( match ) + end + end + end + if param[:notmatch] + it "#{param[:title]}: notmatches" do + param[:notmatch].each do |notmatch| + should_not contain_file("25-#{title}.conf").with_content( notmatch ) + end + end + end + end + end + end + + context 'attribute resources' do + describe 'when access_log_file and access_log_pipe are specified' do + let :params do default_params.merge({ + :access_log_file => 'fake.log', + :access_log_pipe => '| /bin/fake', + }) end + it 'should cause a failure' do + expect { subject }.to raise_error(Puppet::Error, /'access_log_file' and 'access_log_pipe' cannot be defined at the same time/) + end + end + describe 'when error_log_file and error_log_pipe are specified' do + let :params do default_params.merge({ + :error_log_file => 'fake.log', + :error_log_pipe => '| /bin/fake', + }) end + it 'should cause a failure' do + expect { subject }.to raise_error(Puppet::Error, /'error_log_file' and 'error_log_pipe' cannot be defined at the same time/) + end + end + describe 'when docroot owner is specified' do + let :params do default_params.merge({ + :docroot_owner => 'testuser', + :docroot_group => 'testgroup', + }) end + it 'should set vhost ownership' do + should contain_file(params[:docroot]).with({ + :ensure => :directory, + :owner => 'testuser', + :group => 'testgroup', + }) + end + end + + describe 'when wsgi_daemon_process and wsgi_daemon_process_options are specified' do + let :params do default_params.merge({ + :wsgi_daemon_process => 'example.org', + :wsgi_daemon_process_options => { 'processes' => '2', 'threads' => '15' }, + }) end + it 'should set wsgi_daemon_process_options' do + should contain_file("25-#{title}.conf").with_content( + /^ WSGIDaemonProcess example.org processes=2 threads=15$/ + ) + end + end + + describe 'when wsgi_import_script and wsgi_import_script_options are specified' do + let :params do default_params.merge({ + :wsgi_import_script => '/var/www/demo.wsgi', + :wsgi_import_script_options => { 'application-group' => '%{GLOBAL}', 'process-group' => 'wsgi' }, + }) end + it 'should set wsgi_import_script_options' do + should contain_file("25-#{title}.conf").with_content( + /^ WSGIImportScript \/var\/www\/demo.wsgi application-group=%{GLOBAL} process-group=wsgi$/ + ) + end + end + + describe 'when rewrites are specified' do + let :params do default_params.merge({ + :rewrites => [ + { + 'comment' => 'test rewrites', + 'rewrite_base' => '/mytestpath/', + 'rewrite_cond' => ['%{HTTP_USER_AGENT} ^Lynx/ [OR]', '%{HTTP_USER_AGENT} ^Mozilla/[12]'], + 'rewrite_rule' => ['^index\.html$ welcome.html', '^index\.cgi$ index.php'], + } + ] + }) end + it 'should set RewriteConds and RewriteRules' do + should contain_file("25-#{title}.conf").with_content( + /^ #test rewrites$/ + ) + should contain_file("25-#{title}.conf").with_content( + /^ RewriteCond %\{HTTP_USER_AGENT\} \^Lynx\/ \[OR\]$/ + ) + should contain_file("25-#{title}.conf").with_content( + /^ RewriteBase \/mytestpath\/$/ + ) + should contain_file("25-#{title}.conf").with_content( + /^ RewriteCond %\{HTTP_USER_AGENT\} \^Mozilla\/\[12\]$/ + ) + should contain_file("25-#{title}.conf").with_content( + /^ RewriteRule \^index\\.html\$ welcome.html$/ + ) + should contain_file("25-#{title}.conf").with_content( + /^ RewriteRule \^index\\.cgi\$ index.php$/ + ) + end + end + + describe 'when rewrite_rule and rewrite_cond are specified' do + let :params do default_params.merge({ + :rewrite_cond => '%{HTTPS} off', + :rewrite_rule => '(.*) https://%{HTTPS_HOST}%{REQUEST_URI}', + }) end + it 'should set RewriteCond' do + should contain_file("25-#{title}.conf").with_content( + /^ RewriteCond %\{HTTPS\} off$/ + ) + end + end + + describe 'when suphp_engine is on and suphp_configpath is specified' do + let :params do default_params.merge({ + :suphp_engine => 'on', + :suphp_configpath => '/etc/php5/apache2', + }) end + it 'should set suphp_configpath' do + should contain_file("25-#{title}.conf").with_content( + /^ suPHP_ConfigPath "\/etc\/php5\/apache2"$/ + ) + end + end + + describe 'when suphp_engine is on and suphp_addhandler is specified' do + let :params do default_params.merge({ + :suphp_engine => 'on', + :suphp_addhandler => 'x-httpd-php', + }) end + it 'should set suphp_addhandler' do + should contain_file("25-#{title}.conf").with_content( + /^ suPHP_AddHandler x-httpd-php/ + ) + end + end + + describe 'when suphp_engine is on and suphp { user & group } is specified' do + let :params do default_params.merge({ + :suphp_engine => 'on', + :directories => { 'path' => '/srv/www', + 'suphp' => { 'user' => 'myappuser', 'group' => 'myappgroup' }, + } + }) end + it 'should set suphp_UserGroup' do + should contain_file("25-#{title}.conf").with_content( + /^ suPHP_UserGroup myappuser myappgroup/ + ) + end + end + + describe 'priority/default settings' do + describe 'when neither priority/default is specified' do + let :params do default_params end + it { should contain_file("25-#{title}.conf").with_path( + /25-#{title}.conf/ + ) } + end + describe 'when both priority/default_vhost is specified' do + let :params do + default_params.merge({ + :priority => 15, + :default_vhost => true, + }) + end + it { should contain_file("15-#{title}.conf").with_path( + /15-#{title}.conf/ + ) } + end + describe 'when only priority is specified' do + let :params do + default_params.merge({ :priority => 14, }) + end + it { should contain_file("14-#{title}.conf").with_path( + /14-#{title}.conf/ + ) } + end + describe 'when only default is specified' do + let :params do + default_params.merge({ :default_vhost => true, }) + end + it { should contain_file("10-#{title}.conf").with_path( + /10-#{title}.conf/ + ) } + end + end + + describe 'various ip/port combos' do + describe 'when ip_based is true' do + let :params do default_params.merge({ :ip_based => true }) end + it 'should not specify a NameVirtualHost' do + should contain_apache__listen(params[:port]) + should_not contain_apache__namevirtualhost("*:#{params[:port]}") + end + end + + describe 'when ip_based is default' do + let :params do default_params end + it 'should specify a NameVirtualHost' do + should contain_apache__listen(params[:port]) + should contain_apache__namevirtualhost("*:#{params[:port]}") + end + end + + describe 'when an ip is set' do + let :params do default_params.merge({ :ip => '10.0.0.1' }) end + it 'should specify a NameVirtualHost for the ip' do + should_not contain_apache__listen(params[:port]) + should contain_apache__listen("10.0.0.1:#{params[:port]}") + should contain_apache__namevirtualhost("10.0.0.1:#{params[:port]}") + end + end + + describe 'an ip_based vhost without a port' do + let :params do + { + :docroot => '/fake', + :ip => '10.0.0.1', + :ip_based => true, + } + end + it 'should specify a NameVirtualHost for the ip' do + should_not contain_apache__listen(params[:ip]) + should_not contain_apache__namevirtualhost(params[:ip]) + should contain_file("25-#{title}.conf").with_content %r{} + end + end + end + + describe 'redirect rules' do + describe 'without lockstep arrays' do + let :params do + default_params.merge({ + :redirect_source => [ + '/login', + '/logout', + ], + :redirect_dest => [ + 'http://10.0.0.10/login', + 'http://10.0.0.10/logout', + ], + :redirect_status => [ + 'permanent', + '', + ], + }) + end + + it { should contain_file("25-#{title}.conf").with_content %r{ Redirect permanent /login http://10\.0\.0\.10/login} } + it { should contain_file("25-#{title}.conf").with_content %r{ Redirect /logout http://10\.0\.0\.10/logout} } + end + describe 'redirect match rules' do + let :params do + default_params.merge({ + :redirectmatch_status => [ + '404', + ], + :redirectmatch_regexp => [ + '/\.git(/.*|$)', + ], + }) + end + + it { should contain_file("25-#{title}.conf").with_content %r{ RedirectMatch 404 } } + end + describe 'without a status' do + let :params do + default_params.merge({ + :redirect_source => [ + '/login', + '/logout', + ], + :redirect_dest => [ + 'http://10.0.0.10/login', + 'http://10.0.0.10/logout', + ], + }) + end + + it { should contain_file("25-#{title}.conf").with_content %r{ Redirect /login http://10\.0\.0\.10/login} } + it { should contain_file("25-#{title}.conf").with_content %r{ Redirect /logout http://10\.0\.0\.10/logout} } + end + describe 'with a single status and dest' do + let :params do + default_params.merge({ + :redirect_source => [ + '/login', + '/logout', + ], + :redirect_dest => 'http://10.0.0.10/test', + :redirect_status => 'permanent', + }) + end + + it { should contain_file("25-#{title}.conf").with_content %r{ Redirect permanent /login http://10\.0\.0\.10/test} } + it { should contain_file("25-#{title}.conf").with_content %r{ Redirect permanent /logout http://10\.0\.0\.10/test} } + end + + describe 'with a directoryindex specified' do + let :params do + default_params.merge({ + :directoryindex => 'index.php' + }) + end + it { should contain_file("25-#{title}.conf").with_content %r{DirectoryIndex index.php} } + end + end + end + end +end diff --git a/apache/spec/fixtures/modules/site_apache/templates/fake.conf.erb b/apache/spec/fixtures/modules/site_apache/templates/fake.conf.erb new file mode 100644 index 000000000..019debfe4 --- /dev/null +++ b/apache/spec/fixtures/modules/site_apache/templates/fake.conf.erb @@ -0,0 +1 @@ +Fake template for rspec. diff --git a/apache/spec/spec.opts b/apache/spec/spec.opts new file mode 100644 index 000000000..de653df4b --- /dev/null +++ b/apache/spec/spec.opts @@ -0,0 +1,4 @@ +--format s +--colour +--loadby mtime +--backtrace diff --git a/apache/spec/spec_helper.rb b/apache/spec/spec_helper.rb new file mode 100644 index 000000000..2c6f56649 --- /dev/null +++ b/apache/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/apache/spec/spec_helper_acceptance.rb b/apache/spec/spec_helper_acceptance.rb new file mode 100644 index 000000000..7d334ae9b --- /dev/null +++ b/apache/spec/spec_helper_acceptance.rb @@ -0,0 +1,38 @@ +require 'beaker-rspec/spec_helper' +require 'beaker-rspec/helpers/serverspec' + +hosts.each do |host| + if host['platform'] =~ /debian/ + on host, 'echo \'export PATH=/var/lib/gems/1.8/bin/:${PATH}\' >> ~/.bashrc' + end + if host.is_pe? + install_pe + else + install_puppet + on host, "mkdir -p #{host['distmoduledir']}" + end +end + +UNSUPPORTED_PLATFORMS = ['Suse','windows','AIX','Solaris'] + +RSpec.configure do |c| + # Project root + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Readable test descriptions + c.formatter = :documentation + + # Configure all nodes in nodeset + c.before :suite do + # Install module and dependencies + puppet_module_install(:source => proj_root, :module_name => 'apache') + hosts.each do |host| + # Required for mod_passenger tests. + if fact('osfamily') == 'RedHat' + on host, puppet('module','install','stahnma/epel'), { :acceptable_exit_codes => [0,1] } + end + on host, puppet('module','install','puppetlabs-stdlib'), { :acceptable_exit_codes => [0,1] } + on host, puppet('module','install','puppetlabs-concat'), { :acceptable_exit_codes => [0,1] } + end + end +end diff --git a/apache/spec/unit/provider/a2mod/gentoo_spec.rb b/apache/spec/unit/provider/a2mod/gentoo_spec.rb new file mode 100644 index 000000000..ddb9dddda --- /dev/null +++ b/apache/spec/unit/provider/a2mod/gentoo_spec.rb @@ -0,0 +1,184 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +provider_class = Puppet::Type.type(:a2mod).provider(:gentoo) + +describe provider_class do + before :each do + provider_class.clear + end + + [:conf_file, :instances, :modules, :initvars, :conf_file, :clear].each do |method| + it "should respond to the class method #{method}" do + provider_class.should respond_to(method) + end + end + + describe "when fetching modules" do + before do + @filetype = mock() + end + + it "should return a sorted array of the defined parameters" do + @filetype.expects(:read).returns(%Q{APACHE2_OPTS="-D FOO -D BAR -D BAZ"\n}) + provider_class.expects(:filetype).returns(@filetype) + + provider_class.modules.should == %w{bar baz foo} + end + + it "should cache the module list" do + @filetype.expects(:read).once.returns(%Q{APACHE2_OPTS="-D FOO -D BAR -D BAZ"\n}) + provider_class.expects(:filetype).once.returns(@filetype) + + 2.times { provider_class.modules.should == %w{bar baz foo} } + end + + it "should normalize parameters" do + @filetype.expects(:read).returns(%Q{APACHE2_OPTS="-D FOO -D BAR -D BAR"\n}) + provider_class.expects(:filetype).returns(@filetype) + + provider_class.modules.should == %w{bar foo} + end + end + + describe "when prefetching" do + it "should match providers to resources" do + provider = mock("ssl_provider", :name => "ssl") + resource = mock("ssl_resource") + resource.expects(:provider=).with(provider) + + provider_class.expects(:instances).returns([provider]) + provider_class.prefetch("ssl" => resource) + end + end + + describe "when flushing" do + before :each do + @filetype = mock() + @filetype.stubs(:backup) + provider_class.expects(:filetype).at_least_once.returns(@filetype) + + @info = mock() + @info.stubs(:[]).with(:name).returns("info") + @info.stubs(:provider=) + + @mpm = mock() + @mpm.stubs(:[]).with(:name).returns("mpm") + @mpm.stubs(:provider=) + + @ssl = mock() + @ssl.stubs(:[]).with(:name).returns("ssl") + @ssl.stubs(:provider=) + end + + it "should add modules whose ensure is present" do + @filetype.expects(:read).at_least_once.returns(%Q{APACHE2_OPTS=""}) + @filetype.expects(:write).with(%Q{APACHE2_OPTS="-D INFO"}) + + @info.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("info" => @info) + + provider_class.flush + end + + it "should remove modules whose ensure is present" do + @filetype.expects(:read).at_least_once.returns(%Q{APACHE2_OPTS="-D INFO"}) + @filetype.expects(:write).with(%Q{APACHE2_OPTS=""}) + + @info.stubs(:should).with(:ensure).returns(:absent) + @info.stubs(:provider=) + provider_class.prefetch("info" => @info) + + provider_class.flush + end + + it "should not modify providers without resources" do + @filetype.expects(:read).at_least_once.returns(%Q{APACHE2_OPTS="-D INFO -D MPM"}) + @filetype.expects(:write).with(%Q{APACHE2_OPTS="-D MPM -D SSL"}) + + @info.stubs(:should).with(:ensure).returns(:absent) + provider_class.prefetch("info" => @info) + + @ssl.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("ssl" => @ssl) + + provider_class.flush + end + + it "should write the modules in sorted order" do + @filetype.expects(:read).at_least_once.returns(%Q{APACHE2_OPTS=""}) + @filetype.expects(:write).with(%Q{APACHE2_OPTS="-D INFO -D MPM -D SSL"}) + + @mpm.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("mpm" => @mpm) + @info.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("info" => @info) + @ssl.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("ssl" => @ssl) + + provider_class.flush + end + + it "should write the records back once" do + @filetype.expects(:read).at_least_once.returns(%Q{APACHE2_OPTS=""}) + @filetype.expects(:write).once.with(%Q{APACHE2_OPTS="-D INFO -D SSL"}) + + @info.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("info" => @info) + + @ssl.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("ssl" => @ssl) + + provider_class.flush + end + + it "should only modify the line containing APACHE2_OPTS" do + @filetype.expects(:read).at_least_once.returns(%Q{# Comment\nAPACHE2_OPTS=""\n# Another comment}) + @filetype.expects(:write).once.with(%Q{# Comment\nAPACHE2_OPTS="-D INFO"\n# Another comment}) + + @info.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("info" => @info) + provider_class.flush + end + + it "should restore any arbitrary arguments" do + @filetype.expects(:read).at_least_once.returns(%Q{APACHE2_OPTS="-Y -D MPM -X"}) + @filetype.expects(:write).once.with(%Q{APACHE2_OPTS="-Y -X -D INFO -D MPM"}) + + @info.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("info" => @info) + provider_class.flush + end + + it "should backup the file once if changes were made" do + @filetype.expects(:read).at_least_once.returns(%Q{APACHE2_OPTS=""}) + @filetype.expects(:write).once.with(%Q{APACHE2_OPTS="-D INFO -D SSL"}) + + @info.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("info" => @info) + + @ssl.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("ssl" => @ssl) + + @filetype.unstub(:backup) + @filetype.expects(:backup) + provider_class.flush + end + + it "should not write the file or run backups if no changes were made" do + @filetype.expects(:read).at_least_once.returns(%Q{APACHE2_OPTS="-X -D INFO -D SSL -Y"}) + @filetype.expects(:write).never + + @info.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("info" => @info) + + @ssl.stubs(:should).with(:ensure).returns(:present) + provider_class.prefetch("ssl" => @ssl) + + @filetype.unstub(:backup) + @filetype.expects(:backup).never + provider_class.flush + end + end +end diff --git a/apache/templates/confd/no-accf.conf.erb b/apache/templates/confd/no-accf.conf.erb new file mode 100644 index 000000000..10e51644c --- /dev/null +++ b/apache/templates/confd/no-accf.conf.erb @@ -0,0 +1,4 @@ + + AcceptFilter http none + AcceptFilter https none + diff --git a/apache/templates/httpd.conf.erb b/apache/templates/httpd.conf.erb new file mode 100644 index 000000000..66b70836b --- /dev/null +++ b/apache/templates/httpd.conf.erb @@ -0,0 +1,104 @@ +# Security +ServerTokens <%= @server_tokens %> +ServerSignature <%= @server_signature %> +TraceEnable <%= @trace_enable %> + +ServerName "<%= @servername %>" +ServerRoot "<%= @server_root %>" +PidFile <%= @pidfile %> +Timeout <%= @timeout %> +KeepAlive <%= @keepalive %> +MaxKeepAliveRequests 100 +KeepAliveTimeout <%= @keepalive_timeout %> + +User <%= @user %> +Group <%= @group %> + +AccessFileName .htaccess + +<%- if @apache_version >= '2.4' -%> + Require all denied +<%- else -%> + Order allow,deny + Deny from all + Satisfy all +<%- end -%> + + + + Options FollowSymLinks + AllowOverride None + + +DefaultType none +HostnameLookups Off +ErrorLog "<%= @logroot %>/<%= @error_log %>" +LogLevel <%= @log_level %> +EnableSendfile <%= @sendfile %> + +#Listen 80 + +<% if @apxs_workaround -%> +# Workaround: without this hack apxs would be confused about where to put +# LoadModule directives and fail entire procedure of apache package +# installation/reinstallation. This problem was observed on FreeBSD (apache22). +#LoadModule fake_module libexec/apache22/mod_fake.so +<% end -%> + +Include "<%= @mod_load_dir %>/*.load" +<% if @mod_load_dir != @confd_dir and @mod_load_dir != @vhost_load_dir -%> +Include "<%= @mod_load_dir %>/*.conf" +<% end -%> +Include "<%= @ports_file %>" + +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined +LogFormat "%h %l %u %t \"%r\" %>s %b" common +LogFormat "%{Referer}i -> %U" referer +LogFormat "%{User-agent}i" agent + +<%- if @apache_version >= '2.4' -%> +IncludeOptional "<%= @confd_dir %>/*.conf" +<%- else -%> +Include "<%= @confd_dir %>/*.conf" +<%- end -%> +<% if @vhost_load_dir != @confd_dir -%> +Include "<%= @vhost_load_dir %>/*.conf" +<% end -%> + +<% if @error_documents -%> +# /usr/share/apache2/error on debian +Alias /error/ "<%= @error_documents_path %>/" + +"> + AllowOverride None + Options IncludesNoExec + AddOutputFilter Includes html + AddHandler type-map var +<%- if @apache_version == '2.4' -%> + Require all granted +<%- else -%> + Order allow,deny + Allow from all +<%- end -%> + LanguagePriority en cs de es fr it nl sv pt-br ro + ForceLanguagePriority Prefer Fallback + + +ErrorDocument 400 /error/HTTP_BAD_REQUEST.html.var +ErrorDocument 401 /error/HTTP_UNAUTHORIZED.html.var +ErrorDocument 403 /error/HTTP_FORBIDDEN.html.var +ErrorDocument 404 /error/HTTP_NOT_FOUND.html.var +ErrorDocument 405 /error/HTTP_METHOD_NOT_ALLOWED.html.var +ErrorDocument 408 /error/HTTP_REQUEST_TIME_OUT.html.var +ErrorDocument 410 /error/HTTP_GONE.html.var +ErrorDocument 411 /error/HTTP_LENGTH_REQUIRED.html.var +ErrorDocument 412 /error/HTTP_PRECONDITION_FAILED.html.var +ErrorDocument 413 /error/HTTP_REQUEST_ENTITY_TOO_LARGE.html.var +ErrorDocument 414 /error/HTTP_REQUEST_URI_TOO_LARGE.html.var +ErrorDocument 415 /error/HTTP_UNSUPPORTED_MEDIA_TYPE.html.var +ErrorDocument 500 /error/HTTP_INTERNAL_SERVER_ERROR.html.var +ErrorDocument 501 /error/HTTP_NOT_IMPLEMENTED.html.var +ErrorDocument 502 /error/HTTP_BAD_GATEWAY.html.var +ErrorDocument 503 /error/HTTP_SERVICE_UNAVAILABLE.html.var +ErrorDocument 506 /error/HTTP_VARIANT_ALSO_VARIES.html.var +<% end -%> diff --git a/apache/templates/listen.erb b/apache/templates/listen.erb new file mode 100644 index 000000000..8fc871b0a --- /dev/null +++ b/apache/templates/listen.erb @@ -0,0 +1,6 @@ +<%# Listen should always be one of: + - + - : + - [ +-%> +Listen <%= @listen_addr_port %> diff --git a/apache/templates/mod/alias.conf.erb b/apache/templates/mod/alias.conf.erb new file mode 100644 index 000000000..0a0c81593 --- /dev/null +++ b/apache/templates/mod/alias.conf.erb @@ -0,0 +1,13 @@ + +Alias /icons/ "<%= @icons_path %>/" +"> + Options Indexes MultiViews + AllowOverride None +<%- if @apache_version == '2.4' -%> + Require all granted +<%- else -%> + Order allow,deny + Allow from all +<%- end -%> + + diff --git a/apache/templates/mod/authnz_ldap.conf.erb b/apache/templates/mod/authnz_ldap.conf.erb new file mode 100644 index 000000000..565fcf0df --- /dev/null +++ b/apache/templates/mod/authnz_ldap.conf.erb @@ -0,0 +1,5 @@ +<% if @verifyServerCert == true -%> +LDAPVerifyServerCert On +<% else -%> +LDAPVerifyServerCert Off +<% end -%> diff --git a/apache/templates/mod/autoindex.conf.erb b/apache/templates/mod/autoindex.conf.erb new file mode 100644 index 000000000..ef6bbebea --- /dev/null +++ b/apache/templates/mod/autoindex.conf.erb @@ -0,0 +1,56 @@ +IndexOptions FancyIndexing VersionSort HTMLTable NameWidth=* DescriptionWidth=* Charset=UTF-8 +AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip x-bzip2 + +AddIconByType (TXT,/icons/text.gif) text/* +AddIconByType (IMG,/icons/image2.gif) image/* +AddIconByType (SND,/icons/sound2.gif) audio/* +AddIconByType (VID,/icons/movie.gif) video/* + +AddIcon /icons/binary.gif .bin .exe +AddIcon /icons/binhex.gif .hqx +AddIcon /icons/tar.gif .tar +AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv +AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip +AddIcon /icons/a.gif .ps .ai .eps +AddIcon /icons/layout.gif .html .shtml .htm .pdf +AddIcon /icons/text.gif .txt +AddIcon /icons/c.gif .c +AddIcon /icons/p.gif .pl .py +AddIcon /icons/f.gif .for +AddIcon /icons/dvi.gif .dvi +AddIcon /icons/uuencoded.gif .uu +AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl +AddIcon /icons/tex.gif .tex +AddIcon /icons/bomb.gif /core +AddIcon (SND,/icons/sound2.gif) .ogg +AddIcon (VID,/icons/movie.gif) .ogm + +AddIcon /icons/back.gif .. +AddIcon /icons/hand.right.gif README +AddIcon /icons/folder.gif ^^DIRECTORY^^ +AddIcon /icons/blank.gif ^^BLANKICON^^ + +AddIcon /icons/odf6odt-20x22.png .odt +AddIcon /icons/odf6ods-20x22.png .ods +AddIcon /icons/odf6odp-20x22.png .odp +AddIcon /icons/odf6odg-20x22.png .odg +AddIcon /icons/odf6odc-20x22.png .odc +AddIcon /icons/odf6odf-20x22.png .odf +AddIcon /icons/odf6odb-20x22.png .odb +AddIcon /icons/odf6odi-20x22.png .odi +AddIcon /icons/odf6odm-20x22.png .odm + +AddIcon /icons/odf6ott-20x22.png .ott +AddIcon /icons/odf6ots-20x22.png .ots +AddIcon /icons/odf6otp-20x22.png .otp +AddIcon /icons/odf6otg-20x22.png .otg +AddIcon /icons/odf6otc-20x22.png .otc +AddIcon /icons/odf6otf-20x22.png .otf +AddIcon /icons/odf6oti-20x22.png .oti +AddIcon /icons/odf6oth-20x22.png .oth + +DefaultIcon /icons/unknown.gif +ReadmeName README.html +HeaderName HEADER.html + +IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t diff --git a/apache/templates/mod/cgid.conf.erb b/apache/templates/mod/cgid.conf.erb new file mode 100644 index 000000000..5f82d7424 --- /dev/null +++ b/apache/templates/mod/cgid.conf.erb @@ -0,0 +1 @@ +ScriptSock "<%= @cgisock_path %>" diff --git a/apache/templates/mod/dav_fs.conf.erb b/apache/templates/mod/dav_fs.conf.erb new file mode 100644 index 000000000..3c53e9e14 --- /dev/null +++ b/apache/templates/mod/dav_fs.conf.erb @@ -0,0 +1 @@ +DAVLockDB "<%= @dav_lock %>" diff --git a/apache/templates/mod/deflate.conf.erb b/apache/templates/mod/deflate.conf.erb new file mode 100644 index 000000000..d0997dfeb --- /dev/null +++ b/apache/templates/mod/deflate.conf.erb @@ -0,0 +1,4 @@ +AddOutputFilterByType DEFLATE text/html text/plain text/xml +AddOutputFilterByType DEFLATE text/css +AddOutputFilterByType DEFLATE application/x-javascript application/javascript application/ecmascript +AddOutputFilterByType DEFLATE application/rss+xml diff --git a/apache/templates/mod/dir.conf.erb b/apache/templates/mod/dir.conf.erb new file mode 100644 index 000000000..741f6ae03 --- /dev/null +++ b/apache/templates/mod/dir.conf.erb @@ -0,0 +1 @@ +DirectoryIndex <%= @indexes.join(' ') %> diff --git a/apache/templates/mod/disk_cache.conf.erb b/apache/templates/mod/disk_cache.conf.erb new file mode 100644 index 000000000..0c7e2c4b7 --- /dev/null +++ b/apache/templates/mod/disk_cache.conf.erb @@ -0,0 +1,8 @@ + + + CacheEnable disk / + CacheRoot "<%= @cache_root %>" + CacheDirLevels 2 + CacheDirLength 1 + + diff --git a/apache/templates/mod/event.conf.erb b/apache/templates/mod/event.conf.erb new file mode 100644 index 000000000..40099543d --- /dev/null +++ b/apache/templates/mod/event.conf.erb @@ -0,0 +1,9 @@ + + ServerLimit <%= @serverlimit %> + StartServers <%= @startservers %> + MaxClients <%= @maxclients %> + MinSpareThreads <%= @minsparethreads %> + MaxSpareThreads <%= @maxsparethreads %> + ThreadsPerChild <%= @threadsperchild %> + MaxRequestsPerChild <%= @maxrequestsperchild %> + diff --git a/apache/templates/mod/fastcgi.conf.erb b/apache/templates/mod/fastcgi.conf.erb new file mode 100644 index 000000000..8d94a2361 --- /dev/null +++ b/apache/templates/mod/fastcgi.conf.erb @@ -0,0 +1,6 @@ +# The Fastcgi Apache module configuration file is being +# managed by Puppet and changes will be overwritten. + + AddHandler fastcgi-script .fcgi + FastCgiIpcDir "<%= @fastcgi_lib_path %>" + diff --git a/apache/templates/mod/info.conf.erb b/apache/templates/mod/info.conf.erb new file mode 100644 index 000000000..0747da430 --- /dev/null +++ b/apache/templates/mod/info.conf.erb @@ -0,0 +1,10 @@ + + SetHandler server-info + <%- if @apache_version >= '2.4' -%> + Require ip <%= Array(@allow_from).join(" ") %> + <%- else -%> + Order deny,allow + Deny from all + Allow from <%= Array(@allow_from).join(" ") %> + <%- end -%> + diff --git a/apache/templates/mod/itk.conf.erb b/apache/templates/mod/itk.conf.erb new file mode 100644 index 000000000..f45f2b35d --- /dev/null +++ b/apache/templates/mod/itk.conf.erb @@ -0,0 +1,8 @@ + + StartServers <%= @startservers %> + MinSpareServers <%= @minspareservers %> + MaxSpareServers <%= @maxspareservers %> + ServerLimit <%= @serverlimit %> + MaxClients <%= @maxclients %> + MaxRequestsPerChild <%= @maxrequestsperchild %> + diff --git a/apache/templates/mod/ldap.conf.erb b/apache/templates/mod/ldap.conf.erb new file mode 100644 index 000000000..14f33ab2b --- /dev/null +++ b/apache/templates/mod/ldap.conf.erb @@ -0,0 +1,7 @@ + + SetHandler ldap-status + Order deny,allow + Deny from all + Allow from 127.0.0.1 ::1 + Satisfy all + diff --git a/apache/templates/mod/mime.conf.erb b/apache/templates/mod/mime.conf.erb new file mode 100644 index 000000000..a69a424a6 --- /dev/null +++ b/apache/templates/mod/mime.conf.erb @@ -0,0 +1,36 @@ +TypesConfig <%= @mime_types_config %> + +AddType application/x-compress .Z +AddType application/x-gzip .gz .tgz +AddType application/x-bzip2 .bz2 + +AddLanguage ca .ca +AddLanguage cs .cz .cs +AddLanguage da .dk +AddLanguage de .de +AddLanguage el .el +AddLanguage en .en +AddLanguage eo .eo +AddLanguage es .es +AddLanguage et .et +AddLanguage fr .fr +AddLanguage he .he +AddLanguage hr .hr +AddLanguage it .it +AddLanguage ja .ja +AddLanguage ko .ko +AddLanguage ltz .ltz +AddLanguage nl .nl +AddLanguage nn .nn +AddLanguage no .no +AddLanguage pl .po +AddLanguage pt .pt +AddLanguage pt-BR .pt-br +AddLanguage ru .ru +AddLanguage sv .sv +AddLanguage zh-CN .zh-cn +AddLanguage zh-TW .zh-tw + +AddHandler type-map var +AddType text/html .shtml +AddOutputFilter INCLUDES .shtml diff --git a/apache/templates/mod/mime_magic.conf.erb b/apache/templates/mod/mime_magic.conf.erb new file mode 100644 index 000000000..1ce1bc3c1 --- /dev/null +++ b/apache/templates/mod/mime_magic.conf.erb @@ -0,0 +1 @@ +MIMEMagicFile "<%= @magic_file %>" diff --git a/apache/templates/mod/mpm_event.conf.erb b/apache/templates/mod/mpm_event.conf.erb new file mode 100644 index 000000000..eb6f1ff5f --- /dev/null +++ b/apache/templates/mod/mpm_event.conf.erb @@ -0,0 +1,9 @@ + + StartServers 2 + MinSpareThreads 25 + MaxSpareThreads 75 + ThreadLimit 64 + ThreadsPerChild 25 + MaxClients 150 + MaxRequestsPerChild 0 + diff --git a/apache/templates/mod/negotiation.conf.erb b/apache/templates/mod/negotiation.conf.erb new file mode 100644 index 000000000..50921019b --- /dev/null +++ b/apache/templates/mod/negotiation.conf.erb @@ -0,0 +1,2 @@ +LanguagePriority en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv zh-CN zh-TW +ForceLanguagePriority Prefer Fallback diff --git a/apache/templates/mod/nss.conf.erb b/apache/templates/mod/nss.conf.erb new file mode 100644 index 000000000..a5c81752f --- /dev/null +++ b/apache/templates/mod/nss.conf.erb @@ -0,0 +1,228 @@ +# +# This is the Apache server configuration file providing SSL support using. +# the mod_nss plugin. It contains the configuration directives to instruct +# the server how to serve pages over an https connection. +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. You have been warned. +# + +#LoadModule nss_module modules/libmodnss.so + +# +# When we also provide SSL we have to listen to the +# standard HTTP port (see above) and to the HTTPS port +# +# Note: Configurations that use IPv6 but not IPv4-mapped addresses need two +# Listen directives: "Listen [::]:8443" and "Listen 0.0.0.0:443" +# +Listen 8443 + +## +## SSL Global Context +## +## All SSL configuration in this context applies both to +## the main server and all SSL-enabled virtual hosts. +## + +# +# Some MIME-types for downloading Certificates and CRLs +# +AddType application/x-x509-ca-cert .crt +AddType application/x-pkcs7-crl .crl + +# Pass Phrase Dialog: +# Configure the pass phrase gathering process. +# The filtering dialog program (`builtin' is a internal +# terminal dialog) has to provide the pass phrase on stdout. +<% if @passwd_file -%> +NSSPassPhraseDialog "file:<%= @passwd_file %>" +<% else -%> +NSSPassPhraseDialog builtin +<% end -%> + +# Pass Phrase Helper: +# This helper program stores the token password pins between +# restarts of Apache. +NSSPassPhraseHelper /usr/sbin/nss_pcache + +# Configure the SSL Session Cache. +# NSSSessionCacheSize is the number of entries in the cache. +# NSSSessionCacheTimeout is the SSL2 session timeout (in seconds). +# NSSSession3CacheTimeout is the SSL3/TLS session timeout (in seconds). +NSSSessionCacheSize 10000 +NSSSessionCacheTimeout 100 +NSSSession3CacheTimeout 86400 + +# +# Pseudo Random Number Generator (PRNG): +# Configure one or more sources to seed the PRNG of the SSL library. +# The seed data should be of good random quality. +# WARNING! On some platforms /dev/random blocks if not enough entropy +# is available. Those platforms usually also provide a non-blocking +# device, /dev/urandom, which may be used instead. +# +# This does not support seeding the RNG with each connection. + +NSSRandomSeed startup builtin +#NSSRandomSeed startup file:/dev/random 512 +#NSSRandomSeed startup file:/dev/urandom 512 + +# +# TLS Negotiation configuration under RFC 5746 +# +# Only renegotiate if the peer's hello bears the TLS renegotiation_info +# extension. Default off. +NSSRenegotiation off + +# Peer must send Signaling Cipher Suite Value (SCSV) or +# Renegotiation Info (RI) extension in ALL handshakes. Default: off +NSSRequireSafeNegotiation off + +## +## SSL Virtual Host Context +## + + + +# General setup for the virtual host +#DocumentRoot "/etc/httpd/htdocs" +#ServerName www.example.com:8443 +#ServerAdmin you@example.com + +# mod_nss can log to separate log files, you can choose to do that if you'd like +# LogLevel is not inherited from httpd.conf. +ErrorLog "<%= @error_log %>" +TransferLog "<%= @transfer_log %>" +LogLevel warn + +# SSL Engine Switch: +# Enable/Disable SSL for this virtual host. +NSSEngine on + +# SSL Cipher Suite: +# List the ciphers that the client is permitted to negotiate. +# See the mod_nss documentation for a complete list. + +# SSL 3 ciphers. SSL 2 is disabled by default. +NSSCipherSuite +rsa_rc4_128_md5,+rsa_rc4_128_sha,+rsa_3des_sha,-rsa_des_sha,-rsa_rc4_40_md5,-rsa_rc2_40_md5,-rsa_null_md5,-rsa_null_sha,+fips_3des_sha,-fips_des_sha,-fortezza,-fortezza_rc4_128_sha,-fortezza_null,-rsa_des_56_sha,-rsa_rc4_56_sha,+rsa_aes_128_sha,+rsa_aes_256_sha + +# SSL 3 ciphers + ECC ciphers. SSL 2 is disabled by default. +# +# Comment out the NSSCipherSuite line above and use the one below if you have +# ECC enabled NSS and mod_nss and want to use Elliptical Curve Cryptography +#NSSCipherSuite +rsa_rc4_128_md5,+rsa_rc4_128_sha,+rsa_3des_sha,-rsa_des_sha,-rsa_rc4_40_md5,-rsa_rc2_40_md5,-rsa_null_md5,-rsa_null_sha,+fips_3des_sha,-fips_des_sha,-fortezza,-fortezza_rc4_128_sha,-fortezza_null,-rsa_des_56_sha,-rsa_rc4_56_sha,+rsa_aes_128_sha,+rsa_aes_256_sha,-ecdh_ecdsa_null_sha,+ecdh_ecdsa_rc4_128_sha,+ecdh_ecdsa_3des_sha,+ecdh_ecdsa_aes_128_sha,+ecdh_ecdsa_aes_256_sha,-ecdhe_ecdsa_null_sha,+ecdhe_ecdsa_rc4_128_sha,+ecdhe_ecdsa_3des_sha,+ecdhe_ecdsa_aes_128_sha,+ecdhe_ecdsa_aes_256_sha,-ecdh_rsa_null_sha,+ecdh_rsa_128_sha,+ecdh_rsa_3des_sha,+ecdh_rsa_aes_128_sha,+ecdh_rsa_aes_256_sha,-echde_rsa_null,+ecdhe_rsa_rc4_128_sha,+ecdhe_rsa_3des_sha,+ecdhe_rsa_aes_128_sha,+ecdhe_rsa_aes_256_sha + +# SSL Protocol: +# Cryptographic protocols that provide communication security. +# NSS handles the specified protocols as "ranges", and automatically +# negotiates the use of the strongest protocol for a connection starting +# with the maximum specified protocol and downgrading as necessary to the +# minimum specified protocol that can be used between two processes. +# Since all protocol ranges are completely inclusive, and no protocol in the +# middle of a range may be excluded, the entry "NSSProtocol SSLv3,TLSv1.1" +# is identical to the entry "NSSProtocol SSLv3,TLSv1.0,TLSv1.1". +NSSProtocol SSLv3,TLSv1.0,TLSv1.1 + +# SSL Certificate Nickname: +# The nickname of the RSA server certificate you are going to use. +NSSNickname Server-Cert + +# SSL Certificate Nickname: +# The nickname of the ECC server certificate you are going to use, if you +# have an ECC-enabled version of NSS and mod_nss +#NSSECCNickname Server-Cert-ecc + +# Server Certificate Database: +# The NSS security database directory that holds the certificates and +# keys. The database consists of 3 files: cert8.db, key3.db and secmod.db. +# Provide the directory that these files exist. +NSSCertificateDatabase "<%= @httpd_dir -%>/alias" + +# Database Prefix: +# In order to be able to store multiple NSS databases in one directory +# they need unique names. This option sets the database prefix used for +# cert8.db and key3.db. +#NSSDBPrefix my-prefix- + +# Client Authentication (Type): +# Client certificate verification type. Types are none, optional and +# require. +#NSSVerifyClient none + +# +# Online Certificate Status Protocol (OCSP). +# Verify that certificates have not been revoked before accepting them. +#NSSOCSP off + +# +# Use a default OCSP responder. If enabled this will be used regardless +# of whether one is included in a client certificate. Note that the +# server certificate is verified during startup. +# +# NSSOCSPDefaultURL defines the service URL of the OCSP responder +# NSSOCSPDefaultName is the nickname of the certificate to trust to +# sign the OCSP responses. +#NSSOCSPDefaultResponder on +#NSSOCSPDefaultURL http://example.com/ocsp/status +#NSSOCSPDefaultName ocsp-nickname + +# Access Control: +# With SSLRequire you can do per-directory access control based +# on arbitrary complex boolean expressions containing server +# variable checks and other lookup directives. The syntax is a +# mixture between C and Perl. See the mod_nss documentation +# for more details. +# +#NSSRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ +# and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \ +# and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \ +# and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ +# and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ +# or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ +# + +# SSL Engine Options: +# Set various options for the SSL engine. +# o FakeBasicAuth: +# Translate the client X.509 into a Basic Authorisation. This means that +# the standard Auth/DBMAuth methods can be used for access control. The +# user name is the `one line' version of the client's X.509 certificate. +# Note that no password is obtained from the user. Every entry in the user +# file needs this password: `xxj31ZMTZzkVA'. +# o ExportCertData: +# This exports two additional environment variables: SSL_CLIENT_CERT and +# SSL_SERVER_CERT. These contain the PEM-encoded certificates of the +# server (always existing) and the client (only existing when client +# authentication is used). This can be used to import the certificates +# into CGI scripts. +# o StdEnvVars: +# This exports the standard SSL/TLS related `SSL_*' environment variables. +# Per default this exportation is switched off for performance reasons, +# because the extraction step is an expensive operation and is usually +# useless for serving static content. So one usually enables the +# exportation for CGI and SSI requests only. +# o StrictRequire: +# This denies access when "NSSRequireSSL" or "NSSRequire" applied even +# under a "Satisfy any" situation, i.e. when it applies access is denied +# and no other module can change it. +# o OptRenegotiate: +# This enables optimized SSL connection renegotiation handling when SSL +# directives are used in per-directory context. +#NSSOptions +FakeBasicAuth +ExportCertData +CompatEnvVars +StrictRequire + + NSSOptions +StdEnvVars + + + NSSOptions +StdEnvVars + + +# Per-Server Logging: +# The home of a custom SSL log file. Use this when you want a +# compact non-error SSL logfile on a virtual host basis. +#CustomLog /home/rcrit/redhat/apache/logs/ssl_request_log \ +# "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + + diff --git a/apache/templates/mod/passenger.conf.erb b/apache/templates/mod/passenger.conf.erb new file mode 100644 index 000000000..63c3f9e61 --- /dev/null +++ b/apache/templates/mod/passenger.conf.erb @@ -0,0 +1,34 @@ +# The Passanger Apache module configuration file is being +# managed by Puppet and changes will be overwritten. + + <%- if @passenger_root -%> + PassengerRoot "<%= @passenger_root %>" + <%- end -%> + <%- if @passenger_ruby -%> + PassengerRuby "<%= @passenger_ruby %>" + <%- end -%> + <%- if @passenger_high_performance -%> + PassengerHighPerformance <%= @passenger_high_performance %> + <%- end -%> + <%- if @passenger_max_pool_size -%> + PassengerMaxPoolSize <%= @passenger_max_pool_size %> + <%- end -%> + <%- if @passenger_pool_idle_time -%> + PassengerPoolIdleTime <%= @passenger_pool_idle_time %> + <%- end -%> + <%- if @passenger_max_requests -%> + PassengerMaxRequests <%= @passenger_max_requests %> + <%- end -%> + <%- if @passenger_stat_throttle_rate -%> + PassengerStatThrottleRate <%= @passenger_stat_throttle_rate %> + <%- end -%> + <%- if @rack_autodetect -%> + RackAutoDetect <%= @rack_autodetect %> + <%- end -%> + <%- if @rails_autodetect -%> + RailsAutoDetect <%= @rails_autodetect %> + <%- end -%> + <%- if @passenger_use_global_queue -%> + PassengerUseGlobalQueue <%= @passenger_use_global_queue %> + <%- end -%> + diff --git a/apache/templates/mod/peruser.conf.erb b/apache/templates/mod/peruser.conf.erb new file mode 100644 index 000000000..13c8d708d --- /dev/null +++ b/apache/templates/mod/peruser.conf.erb @@ -0,0 +1,12 @@ + + MinSpareProcessors <%= @minspareprocessors %> + MinProcessors <%= @minprocessors %> + MaxProcessors <%= @maxprocessors %> + MaxClients <%= @maxclients %> + MaxRequestsPerChild <%= @maxrequestsperchild %> + IdleTimeout <%= @idletimeout %> + ExpireTimeout <%= @expiretimeout %> + KeepAlive <%= @keepalive %> + Include "<%= @mod_dir %>/peruser/multiplexers/*.conf" + Include "<%= @mod_dir %>/peruser/processors/*.conf" + diff --git a/apache/templates/mod/php5.conf.erb b/apache/templates/mod/php5.conf.erb new file mode 100644 index 000000000..9eef7628a --- /dev/null +++ b/apache/templates/mod/php5.conf.erb @@ -0,0 +1,30 @@ +# +# PHP is an HTML-embedded scripting language which attempts to make it +# easy for developers to write dynamically generated webpages. +# +# +# LoadModule php5_module modules/libphp5.so +# +# +# # Use of the "ZTS" build with worker is experimental, and no shared +# # modules are supported. +# LoadModule php5_module modules/libphp5-zts.so +# + +# +# Cause the PHP interpreter to handle files with a .php extension. +# +AddHandler php5-script .php +AddType text/html .php + +# +# Add index.php to the list of files that will be served as directory +# indexes. +# +DirectoryIndex index.php + +# +# Uncomment the following line to allow PHP to pretty-print .phps +# files as PHP source code: +# +#AddType application/x-httpd-php-source .phps diff --git a/apache/templates/mod/prefork.conf.erb b/apache/templates/mod/prefork.conf.erb new file mode 100644 index 000000000..aabfdf7b2 --- /dev/null +++ b/apache/templates/mod/prefork.conf.erb @@ -0,0 +1,8 @@ + + StartServers <%= @startservers %> + MinSpareServers <%= @minspareservers %> + MaxSpareServers <%= @maxspareservers %> + ServerLimit <%= @serverlimit %> + MaxClients <%= @maxclients %> + MaxRequestsPerChild <%= @maxrequestsperchild %> + diff --git a/apache/templates/mod/proxy.conf.erb b/apache/templates/mod/proxy.conf.erb new file mode 100644 index 000000000..1f4a4129c --- /dev/null +++ b/apache/templates/mod/proxy.conf.erb @@ -0,0 +1,23 @@ +# +# Proxy Server directives. Uncomment the following lines to +# enable the proxy server: +# + + # Do not enable proxying with ProxyRequests until you have secured your + # server. Open proxy servers are dangerous both to your network and to the + # Internet at large. + ProxyRequests <%= @proxy_requests %> + + <% if @proxy_requests != 'Off' or ( @allow_from and ! @allow_from.empty? ) -%> + + Order deny,allow + Deny from all + Allow from <%= Array(@allow_from).join(" ") %> + + <% end -%> + + # Enable/disable the handling of HTTP/1.1 "Via:" headers. + # ("Full" adds the server version; "Block" removes all outgoing Via: headers) + # Set to one of: Off | On | Full | Block + ProxyVia On + diff --git a/apache/templates/mod/proxy_html.conf.erb b/apache/templates/mod/proxy_html.conf.erb new file mode 100644 index 000000000..7f5898ef7 --- /dev/null +++ b/apache/templates/mod/proxy_html.conf.erb @@ -0,0 +1,24 @@ +<% if @proxy_html_loadfiles -%> +<% Array(@proxy_html_loadfiles).each do |loadfile| -%> +LoadFile <%= loadfile %> +<% end -%> + +<% end -%> +ProxyHTMLLinks a href +ProxyHTMLLinks area href +ProxyHTMLLinks link href +ProxyHTMLLinks img src longdesc usemap +ProxyHTMLLinks object classid codebase data usemap +ProxyHTMLLinks q cite +ProxyHTMLLinks blockquote cite +ProxyHTMLLinks ins cite +ProxyHTMLLinks del cite +ProxyHTMLLinks form action +ProxyHTMLLinks input src usemap +ProxyHTMLLinks head profileProxyHTMLLinks base href +ProxyHTMLLinks script src for + +ProxyHTMLEvents onclick ondblclick onmousedown onmouseup \ + onmouseover onmousemove onmouseout onkeypress \ + onkeydown onkeyup onfocus onblur onload \ + onunload onsubmit onreset onselect onchange diff --git a/apache/templates/mod/reqtimeout.conf.erb b/apache/templates/mod/reqtimeout.conf.erb new file mode 100644 index 000000000..9a18800da --- /dev/null +++ b/apache/templates/mod/reqtimeout.conf.erb @@ -0,0 +1,2 @@ +RequestReadTimeout header=20-40,minrate=500 +RequestReadTimeout body=10,minrate=500 diff --git a/apache/templates/mod/rpaf.conf.erb b/apache/templates/mod/rpaf.conf.erb new file mode 100644 index 000000000..56e2398b5 --- /dev/null +++ b/apache/templates/mod/rpaf.conf.erb @@ -0,0 +1,15 @@ +# Enable reverse proxy add forward +RPAFenable On +# RPAFsethostname will, when enabled, take the incoming X-Host header and +# update the virtual host settings accordingly. This allows to have the same +# hostnames as in the "real" configuration for the forwarding proxy. +<% if @sethostname -%> +RPAFsethostname On +<% else -%> +RPAFsethostname Off +<% end -%> +# Which IPs are forwarding requests to us +RPAFproxy_ips <%= Array(@proxy_ips).join(" ") %> +# Setting RPAFheader allows you to change the header name to parse from the +# default X-Forwarded-For to something of your choice. +RPAFheader <%= @header %> diff --git a/apache/templates/mod/setenvif.conf.erb b/apache/templates/mod/setenvif.conf.erb new file mode 100644 index 000000000..d31c79fe5 --- /dev/null +++ b/apache/templates/mod/setenvif.conf.erb @@ -0,0 +1,34 @@ +# +# The following directives modify normal HTTP response behavior to +# handle known problems with browser implementations. +# +BrowserMatch "Mozilla/2" nokeepalive +BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 +BrowserMatch "RealPlayer 4\.0" force-response-1.0 +BrowserMatch "Java/1\.0" force-response-1.0 +BrowserMatch "JDK/1\.0" force-response-1.0 + +# +# The following directive disables redirects on non-GET requests for +# a directory that does not include the trailing slash. This fixes a +# problem with Microsoft WebFolders which does not appropriately handle +# redirects for folders with DAV methods. +# Same deal with Apple's DAV filesystem and Gnome VFS support for DAV. +# +BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully +BrowserMatch "MS FrontPage" redirect-carefully +BrowserMatch "^WebDrive" redirect-carefully +BrowserMatch "^WebDAVFS/1.[0123]" redirect-carefully +BrowserMatch "^gnome-vfs/1.0" redirect-carefully +BrowserMatch "^gvfs/1" redirect-carefully +BrowserMatch "^XML Spy" redirect-carefully +BrowserMatch "^Dreamweaver-WebDAV-SCM1" redirect-carefully +BrowserMatch " Konqueror/4" redirect-carefully + + + BrowserMatch "MSIE [2-6]" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + # MSIE 7 and newer should be able to use keepalive + BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown + diff --git a/apache/templates/mod/ssl.conf.erb b/apache/templates/mod/ssl.conf.erb new file mode 100644 index 000000000..e1597f2f8 --- /dev/null +++ b/apache/templates/mod/ssl.conf.erb @@ -0,0 +1,28 @@ + + SSLRandomSeed startup builtin + SSLRandomSeed startup file:/dev/urandom 512 + SSLRandomSeed connect builtin + SSLRandomSeed connect file:/dev/urandom 512 + + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + + SSLPassPhraseDialog builtin + SSLSessionCache "shmcb:<%= @session_cache %>" + SSLSessionCacheTimeout 300 +<% if @ssl_compression -%> + SSLCompression Off +<% end -%> + <% if @apache_version >= '2.4' -%> + Mutex <%= @ssl_mutex %> + <% else -%> + SSLMutex <%= @ssl_mutex %> + <% end -%> + SSLCryptoDevice builtin + SSLHonorCipherOrder On + SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5 + SSLProtocol all -SSLv2 +<% if @ssl_options -%> + SSLOptions <%= @ssl_options.compact.join(' ') %> +<% end -%> + diff --git a/apache/templates/mod/status.conf.erb b/apache/templates/mod/status.conf.erb new file mode 100644 index 000000000..c00c16a78 --- /dev/null +++ b/apache/templates/mod/status.conf.erb @@ -0,0 +1,12 @@ + + SetHandler server-status + Order deny,allow + Deny from all + Allow from <%= Array(@allow_from).join(" ") %> + +ExtendedStatus <%= @extended_status %> + + + # Show Proxy LoadBalancer status in mod_status + ProxyStatus On + diff --git a/apache/templates/mod/suphp.conf.erb b/apache/templates/mod/suphp.conf.erb new file mode 100644 index 000000000..95fbf97c7 --- /dev/null +++ b/apache/templates/mod/suphp.conf.erb @@ -0,0 +1,19 @@ + + AddType application/x-httpd-suphp .php .php3 .php4 .php5 .phtml + suPHP_AddHandler application/x-httpd-suphp + + + suPHP_Engine on + + + # By default, disable suPHP for debian packaged web applications as files + # are owned by root and cannot be executed by suPHP because of min_uid. + + suPHP_Engine off + + +# # Use a specific php config file (a dir which contains a php.ini file) +# suPHP_ConfigPath /etc/php4/cgi/suphp/ +# # Tells mod_suphp NOT to handle requests with the type . +# suPHP_RemoveHandler + diff --git a/apache/templates/mod/userdir.conf.erb b/apache/templates/mod/userdir.conf.erb new file mode 100644 index 000000000..e4c6ba55d --- /dev/null +++ b/apache/templates/mod/userdir.conf.erb @@ -0,0 +1,19 @@ + +<% if @disable_root -%> + UserDir disabled root +<% end -%> + UserDir <%= @dir %> + + /*/<%= @dir %>"> + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + + Order allow,deny + Allow from all + + + Order deny,allow + Deny from all + + + diff --git a/apache/templates/mod/worker.conf.erb b/apache/templates/mod/worker.conf.erb new file mode 100644 index 000000000..f0bba3908 --- /dev/null +++ b/apache/templates/mod/worker.conf.erb @@ -0,0 +1,9 @@ + + ServerLimit <%= @serverlimit %> + StartServers <%= @startservers %> + MaxClients <%= @maxclients %> + MinSpareThreads <%= @minsparethreads %> + MaxSpareThreads <%= @maxsparethreads %> + ThreadsPerChild <%= @threadsperchild %> + MaxRequestsPerChild <%= @maxrequestsperchild %> + diff --git a/apache/templates/mod/wsgi.conf.erb b/apache/templates/mod/wsgi.conf.erb new file mode 100644 index 000000000..18752d2c4 --- /dev/null +++ b/apache/templates/mod/wsgi.conf.erb @@ -0,0 +1,13 @@ +# The WSGI Apache module configuration file is being +# managed by Puppet an changes will be overwritten. + + <%- if @wsgi_socket_prefix -%> + WSGISocketPrefix <%= @wsgi_socket_prefix %> + <%- end -%> + <%- if @wsgi_python_home -%> + WSGIPythonHome "<%= @wsgi_python_home %>" + <%- end -%> + <%- if @wsgi_python_path -%> + WSGIPythonPath "<%= @wsgi_python_path %>" + <%- end -%> + diff --git a/apache/templates/namevirtualhost.erb b/apache/templates/namevirtualhost.erb new file mode 100644 index 000000000..cf767680f --- /dev/null +++ b/apache/templates/namevirtualhost.erb @@ -0,0 +1,8 @@ +<%# NameVirtualHost should always be one of: + - * + - *: + - _default_: + - + - : +-%> +NameVirtualHost <%= @addr_port %> diff --git a/apache/templates/ports_header.erb b/apache/templates/ports_header.erb new file mode 100644 index 000000000..4908db4ad --- /dev/null +++ b/apache/templates/ports_header.erb @@ -0,0 +1,5 @@ +# ************************************ +# Listen & NameVirtualHost resources in module puppetlabs-apache +# Managed by Puppet +# ************************************ + diff --git a/apache/templates/vhost.conf.erb b/apache/templates/vhost.conf.erb new file mode 100644 index 000000000..0eb69b009 --- /dev/null +++ b/apache/templates/vhost.conf.erb @@ -0,0 +1,64 @@ +# ************************************ +# Vhost template in module puppetlabs-apache +# Managed by Puppet +# ************************************ + +> + ServerName <%= @servername %> +<% if @serveradmin -%> + ServerAdmin <%= @serveradmin %> +<% end -%> + + ## Vhost docroot +<% if @virtual_docroot -%> + VirtualDocumentRoot "<%= @virtual_docroot %>" +<% else -%> + DocumentRoot "<%= @docroot %>" +<% end -%> +<%= scope.function_template(['apache/vhost/_aliases.erb']) -%> + +<%= scope.function_template(['apache/vhost/_itk.erb']) -%> + +<% if @fallbackresource -%> + FallbackResource <%= @fallbackresource %> +<% end -%> + + ## Directories, there should at least be a declaration for <%= @docroot %> +<%= scope.function_template(['apache/vhost/_directories.erb']) -%> + + ## Load additional static includes +<% Array(@additional_includes).each do |include| %> + Include "<%= include %>" +<% end %> + + ## Logging +<% if @error_log -%> + ErrorLog "<%= @error_log_destination %>" +<% end -%> +<% if @log_level -%> + LogLevel <%= @log_level %> +<% end -%> + ServerSignature Off +<% if @access_log and @_access_log_env_var -%> + CustomLog "<%= @access_log_destination %>" <%= @_access_log_format %> <%= @_access_log_env_var %> +<% elsif @access_log -%> + CustomLog "<%= @access_log_destination %>" <%= @_access_log_format %> +<% end -%> +<%= scope.function_template(['apache/vhost/_block.erb']) -%> +<%= scope.function_template(['apache/vhost/_error_document.erb']) -%> +<%= scope.function_template(['apache/vhost/_proxy.erb']) -%> +<%= scope.function_template(['apache/vhost/_rack.erb']) -%> +<%= scope.function_template(['apache/vhost/_redirect.erb']) -%> +<%= scope.function_template(['apache/vhost/_rewrite.erb']) -%> +<%= scope.function_template(['apache/vhost/_scriptalias.erb']) -%> +<%= scope.function_template(['apache/vhost/_serveralias.erb']) -%> +<%= scope.function_template(['apache/vhost/_setenv.erb']) -%> +<%= scope.function_template(['apache/vhost/_ssl.erb']) -%> +<%= scope.function_template(['apache/vhost/_suphp.erb']) -%> +<%= scope.function_template(['apache/vhost/_php_admin.erb']) -%> +<%= scope.function_template(['apache/vhost/_header.erb']) -%> +<%= scope.function_template(['apache/vhost/_requestheader.erb']) -%> +<%= scope.function_template(['apache/vhost/_wsgi.erb']) -%> +<%= scope.function_template(['apache/vhost/_custom_fragment.erb']) -%> +<%= scope.function_template(['apache/vhost/_fastcgi.erb']) -%> + diff --git a/apache/templates/vhost/_aliases.erb b/apache/templates/vhost/_aliases.erb new file mode 100644 index 000000000..5fdd76ba2 --- /dev/null +++ b/apache/templates/vhost/_aliases.erb @@ -0,0 +1,12 @@ +<% if @aliases and ! @aliases.empty? -%> + ## Alias declarations for resources outside the DocumentRoot + <%- [@aliases].flatten.compact.each do |alias_statement| -%> + <%- if alias_statement["path"] != '' -%> + <%- if alias_statement["alias"] and alias_statement["alias"] != '' -%> + Alias <%= alias_statement["alias"] %> "<%= alias_statement["path"] %>" + <%- elsif alias_statement["aliasmatch"] and alias_statement["aliasmatch"] != '' -%> + AliasMatch <%= alias_statement["aliasmatch"] %> "<%= alias_statement["path"] %>" + <%- end -%> + <%- end -%> + <%- end -%> +<% end -%> diff --git a/apache/templates/vhost/_block.erb b/apache/templates/vhost/_block.erb new file mode 100644 index 000000000..f3c835d2c --- /dev/null +++ b/apache/templates/vhost/_block.erb @@ -0,0 +1,14 @@ +<% if @block and ! @block.empty? -%> + + ## Block access statements +<% if @block.include? 'scm' -%> + # Block access to SCM directories. + + <%- if @apache_version >= '2.4' -%> + Require all denied + <%- else -%> + Deny From All + <%- end -%> + +<% end -%> +<% end -%> diff --git a/apache/templates/vhost/_custom_fragment.erb b/apache/templates/vhost/_custom_fragment.erb new file mode 100644 index 000000000..973964655 --- /dev/null +++ b/apache/templates/vhost/_custom_fragment.erb @@ -0,0 +1,5 @@ +<% if @custom_fragment -%> + + ## Custom fragment +<%= @custom_fragment %> +<% end -%> diff --git a/apache/templates/vhost/_directories.erb b/apache/templates/vhost/_directories.erb new file mode 100644 index 000000000..516d0798d --- /dev/null +++ b/apache/templates/vhost/_directories.erb @@ -0,0 +1,159 @@ +<% if @_directories and ! @_directories.empty? -%> + <%- [@_directories].flatten.compact.each do |directory| -%> + <%- if directory['path'] and directory['path'] != '' -%> + <%- if directory['provider'] and directory['provider'].match('(directory|location|files)') -%> + <%- if /^(.*)match$/ =~ directory['provider'] -%> + <%- provider = $1.capitalize + 'Match' -%> + <%- else -%> + <%- provider = directory['provider'].capitalize -%> + <%- end -%> + <%- else -%> + <%- provider = 'Directory' -%> + <%- end -%> + <%- path = directory['path'] %> + + <<%= provider %> "<%= path %>"> + <%- if directory['headers'] -%> + <%- Array(directory['headers']).each do |header| -%> + Header <%= header %> + <%- end -%> + <%- end -%> + <%- if directory['options'] -%> + Options <%= Array(directory['options']).join(' ') %> + <%- end -%> + <%- if provider == 'Directory' -%> + <%- if directory['index_options'] -%> + IndexOptions <%= Array(directory['index_options']).join(' ') %> + <%- end -%> + <%- if directory['index_order_default'] -%> + IndexOrderDefault <%= Array(directory['index_order_default']).join(' ') %> + <%- end -%> + <%- if directory['allow_override'] -%> + AllowOverride <%= Array(directory['allow_override']).join(' ') %> + <%- elsif provider == 'Directory' -%> + AllowOverride None + <%- end -%> + <%- end -%> + <%- if @apache_version == '2.4' -%> + <%- if directory['require'] and directory['require'] != '' -%> + Require <%= Array(directory['require']).join(' ') %> + <%- else -%> + Require all granted + <%- end -%> + <%- else -%> + <%- if directory['order'] and directory['order'] != '' -%> + Order <%= Array(directory['order']).join(',') %> + <%- else -%> + Order allow,deny + <%- end -%> + <%- if directory['deny'] and directory['deny'] != '' -%> + Deny <%= directory['deny'] %> + <%- end -%> + <%- if directory['allow'] and ! [ false, 'false', '' ].include?(directory['allow']) -%> + Allow <%= directory['allow'] %> + <%- elsif [ 'from all', 'from All' ].include?(directory['deny']) -%> + <%- elsif ! directory['deny'] and [ false, 'false', '' ].include?(directory['allow']) -%> + Deny from all + <%- else -%> + Allow from all + <%- end -%> + <%- end -%> + <%- if directory['addhandlers'] and ! directory['addhandlers'].empty? -%> + <%- [directory['addhandlers']].flatten.compact.each do |addhandler| -%> + AddHandler <%= addhandler['handler'] %> <%= Array(addhandler['extensions']).join(' ') %> + <%- end -%> + <%- end -%> + <%- if directory['passenger_enabled'] and directory['passenger_enabled'] != '' -%> + PassengerEnabled <%= directory['passenger_enabled'] %> + <%- end -%> + <%- if directory['php_admin_flags'] and ! directory['php_admin_flags'].empty? -%> + <%- directory['php_admin_flags'].each do |flag,value| -%> + <%- value = if value =~ /true|yes|on|1/i then 'on' else 'off' end -%> + php_admin_flag <%= "#{flag} #{value}" %> + <%- end -%> + <%- end -%> + <%- if directory['php_admin_values'] and ! directory['php_admin_values'].empty? -%> + <%- directory['php_admin_values'].each do |key,value| -%> + php_admin_value <%= "#{key} #{value}" %> + <%- end -%> + <%- end -%> + <%- if directory['directoryindex'] and directory['directoryindex'] != '' -%> + DirectoryIndex <%= directory['directoryindex'] %> + <%- end -%> + <%- if directory['error_documents'] and ! directory['error_documents'].empty? -%> + <%- [directory['error_documents']].flatten.compact.each do |error_document| -%> + ErrorDocument <%= error_document['error_code'] %> <%= error_document['document'] %> + <%- end -%> + <%- end -%> + <%- if directory['auth_type'] -%> + AuthType <%= directory['auth_type'] %> + <%- end -%> + <%- if directory['auth_name'] -%> + AuthName "<%= directory['auth_name'] %>" + <%- end -%> + <%- if directory['auth_digest_algorithm'] -%> + AuthDigestAlgorithm <%= directory['auth_digest_algorithm'] %> + <%- end -%> + <%- if directory['auth_digest_domain'] -%> + AuthDigestDomain <%= Array(directory['auth_digest_domain']).join(' ') %> + <%- end -%> + <%- if directory['auth_digest_nonce_lifetime'] -%> + AuthDigestNonceLifetime <%= directory['auth_digest_nonce_lifetime'] %> + <%- end -%> + <%- if directory['auth_digest_provider'] -%> + AuthDigestProvider <%= directory['auth_digest_provider'] %> + <%- end -%> + <%- if directory['auth_digest_qop'] -%> + AuthDigestQop <%= directory['auth_digest_qop'] %> + <%- end -%> + <%- if directory['auth_digest_shmem_size'] -%> + AuthDigestShmemSize <%= directory['auth_digest_shmem_size'] %> + <%- end -%> + <%- if directory['auth_basic_authoritative'] -%> + AuthBasicAuthoritative <%= directory['auth_basic_authoritative'] %> + <%- end -%> + <%- if directory['auth_basic_fake'] -%> + AuthBasicFake <%= directory['auth_basic_fake'] %> + <%- end -%> + <%- if directory['auth_basic_provider'] -%> + AuthBasicProvider <%= directory['auth_basic_provider'] %> + <%- end -%> + <%- if directory['auth_user_file'] -%> + AuthUserFile <%= directory['auth_user_file'] %> + <%- end -%> + <%- if directory['auth_group_file'] -%> + AuthGroupFile <%= directory['auth_group_file'] %> + <%- end -%> + <%- if directory['auth_require'] -%> + Require <%= directory['auth_require'] %> + <%- end -%> + <%- if directory['fallbackresource'] -%> + FallbackResource <%= directory['fallbackresource'] %> + <%- end -%> + <%- if directory['expires_active'] -%> + ExpiresActive <%= directory['expires_active'] %> + <%- end -%> + <%- if directory['expires_default'] -%> + ExpiresDefault <%= directory['expires_default'] %> + <%- end -%> + <%- if directory['expires_by_type'] -%> + <%- Array(directory['expires_by_type']).each do |rule| -%> + ExpiresByType <%= rule %> + <%- end -%> + <%- end -%> + <%- if directory['force_type'] -%> + ForceType <%= directory['force_type'] %> + <%- end -%> + <%- if directory['ssl_options'] -%> + SSLOptions <%= Array(directory['ssl_options']).join(' ') %> + <%- end -%> + <%- if directory['suphp'] and @suphp_engine == 'on' -%> + suPHP_UserGroup <%= directory['suphp']['user'] %> <%= directory['suphp']['group'] %> + <%- end -%> + <%- if directory['custom_fragment'] -%> + <%= directory['custom_fragment'] %> + <%- end -%> + > + <%- end -%> + <%- end -%> +<% end -%> diff --git a/apache/templates/vhost/_error_document.erb b/apache/templates/vhost/_error_document.erb new file mode 100644 index 000000000..654e72c67 --- /dev/null +++ b/apache/templates/vhost/_error_document.erb @@ -0,0 +1,7 @@ +<% if @error_documents and ! @error_documents.empty? -%> + <%- [@error_documents].flatten.compact.each do |error_document| -%> + <%- if error_document["error_code"] != '' and error_document["document"] != '' -%> + ErrorDocument <%= error_document["error_code"] %> <%= error_document["document"] %> + <%- end -%> + <%- end -%> +<% end -%> diff --git a/apache/templates/vhost/_fastcgi.erb b/apache/templates/vhost/_fastcgi.erb new file mode 100644 index 000000000..07129bc19 --- /dev/null +++ b/apache/templates/vhost/_fastcgi.erb @@ -0,0 +1,22 @@ +<% if @fastcgi_server -%> + + FastCgiExternalServer <%= @fastcgi_server %> -socket <%= @fastcgi_socket %> +<% end -%> +<% if @fastcgi_dir -%> + + "> + Options +ExecCGI + AllowOverride All + SetHandler fastcgi-script + <%- if @apache_version >= '2.4' -%> + Require all granted + <%- else -%> + Order allow,deny + Allow From All + <%- end -%> + AuthBasicAuthoritative Off + + + AllowEncodedSlashes On + ServerSignature Off +<% end -%> diff --git a/apache/templates/vhost/_header.erb b/apache/templates/vhost/_header.erb new file mode 100644 index 000000000..c0f68c825 --- /dev/null +++ b/apache/templates/vhost/_header.erb @@ -0,0 +1,10 @@ +<% if @headers and ! @headers.empty? -%> + + ## Header rules + ## as per http://httpd.apache.org/docs/2.2/mod/mod_headers.html#header + <%- Array(@headers).each do |header_statement| -%> + <%- if header_statement != '' -%> + Header <%= header_statement %> + <%- end -%> + <%- end -%> +<% end -%> diff --git a/apache/templates/vhost/_itk.erb b/apache/templates/vhost/_itk.erb new file mode 100644 index 000000000..2971c7a7d --- /dev/null +++ b/apache/templates/vhost/_itk.erb @@ -0,0 +1,28 @@ +<% if @itk and ! @itk.empty? -%> + ## ITK statement + + <%- if @itk["user"] and @itk["group"] -%> + AssignUserId <%= @itk["user"] %> <%= @itk["group"] %> + <%- end -%> + <%- if @itk["assignuseridexpr"] -%> + AssignUserIdExpr <%= @itk["assignuseridexpr"] %> + <%- end -%> + <%- if @itk["assigngroupidexpr"] -%> + AssignGroupIdExpr <%= @itk["assigngroupidexpr"] %> + <%- end -%> + <%- if @itk["maxclientvhost"] -%> + MaxClientsVHost <%= @itk["maxclientvhost"] %> + <%- end -%> + <%- if @itk["nice"] -%> + NiceValue <%= @itk["nice"] %> + <%- end -%> + <%- if @kernelversion >= '3.5.0' -%> + <%- if @itk["limituidrange"] -%> + LimitUIDRange <%= @itk["limituidrange"] %> + <%- end -%> + <%- if @itk["limitgidrange"] -%> + LimitGIDRange <%= @itk["limitgidrange"] %> + <%- end -%> + <%- end -%> + +<% end -%> diff --git a/apache/templates/vhost/_php_admin.erb b/apache/templates/vhost/_php_admin.erb new file mode 100644 index 000000000..59536cbc9 --- /dev/null +++ b/apache/templates/vhost/_php_admin.erb @@ -0,0 +1,12 @@ +<% if @php_admin_values and not @php_admin_values.empty? -%> +<% @php_admin_values.each do |key,value| -%> + php_admin_value <%= key %> <%= value %> +<% end -%> +<% end -%> +<% if @php_admin_flags and not @php_admin_flags.empty? -%> +<% @php_admin_flags.each do |key,flag| -%> +<%# normalize flag -%> +<% if flag =~ /true|yes|on|1/i then flag = 'on' else flag = 'off' end -%> + php_admin_flag <%= key %> <%= flag %> +<% end -%> +<% end -%> diff --git a/apache/templates/vhost/_proxy.erb b/apache/templates/vhost/_proxy.erb new file mode 100644 index 000000000..7e0221f95 --- /dev/null +++ b/apache/templates/vhost/_proxy.erb @@ -0,0 +1,20 @@ +<% if @proxy_dest or @proxy_pass -%> + + ## Proxy rules + ProxyRequests Off +<%- end -%> +<% [@proxy_pass].flatten.compact.each do |proxy| %> + ProxyPass <%= proxy['path'] %> <%= proxy['url'] %> + > + ProxyPassReverse <%= proxy['url'] %> + +<% end %> +<% if @proxy_dest -%> +<% Array(@no_proxy_uris).each do |uri| %> + ProxyPass <%= uri %> ! +<% end %> + ProxyPass / <%= @proxy_dest %>/ + + ProxyPassReverse <%= @proxy_dest %>/ + +<% end -%> diff --git a/apache/templates/vhost/_rack.erb b/apache/templates/vhost/_rack.erb new file mode 100644 index 000000000..4a5b5f1cd --- /dev/null +++ b/apache/templates/vhost/_rack.erb @@ -0,0 +1,7 @@ +<% if @rack_base_uris -%> + + ## Enable rack +<% Array(@rack_base_uris).each do |uri| -%> + RackBaseURI <%= uri %> +<% end -%> +<% end -%> diff --git a/apache/templates/vhost/_redirect.erb b/apache/templates/vhost/_redirect.erb new file mode 100644 index 000000000..e865bd9af --- /dev/null +++ b/apache/templates/vhost/_redirect.erb @@ -0,0 +1,24 @@ +<% if @redirect_source and @redirect_dest -%> +<% @redirect_dest_a = Array(@redirect_dest) -%> +<% @redirect_source_a = Array(@redirect_source) -%> +<% @redirect_status_a = Array(@redirect_status) -%> + + ## Redirect rules +<% @redirect_source_a.each_with_index do |source, i| -%> +<% @redirect_dest_a[i] ||= @redirect_dest_a[0] -%> +<% @redirect_status_a[i] ||= @redirect_status_a[0] -%> + Redirect <%= "#{@redirect_status_a[i]} " %><%= source %> <%= @redirect_dest_a[i] %> +<% end -%> +<% end -%> + +<%- if @redirectmatch_status and @redirectmatch_regexp -%> +<% @redirectmatch_status_a = Array(@redirectmatch_status) -%> +<% @redirectmatch_regexp_a = Array(@redirectmatch_regexp) -%> + + ## RedirectMatch rules +<% @redirectmatch_status_a.each_with_index do |status, i| -%> +<% @redirectmatch_status_a[i] ||= @redirectmatch_status_a[0] -%> +<% @redirectmatch_regexp_a[i] ||= @redirectmatch_regexp_a[0] -%> + RedirectMatch <%= "#{@redirectmatch_status_a[i]} " %> <%= @redirectmatch_regexp_a[i] %> +<% end -%> +<% end -%> diff --git a/apache/templates/vhost/_requestheader.erb b/apache/templates/vhost/_requestheader.erb new file mode 100644 index 000000000..9f175052b --- /dev/null +++ b/apache/templates/vhost/_requestheader.erb @@ -0,0 +1,10 @@ +<% if @request_headers and ! @request_headers.empty? -%> + + ## Request header rules + ## as per http://httpd.apache.org/docs/2.2/mod/mod_headers.html#requestheader + <%- Array(@request_headers).each do |request_statement| -%> + <%- if request_statement != '' -%> + RequestHeader <%= request_statement %> + <%- end -%> + <%- end -%> +<% end -%> diff --git a/apache/templates/vhost/_rewrite.erb b/apache/templates/vhost/_rewrite.erb new file mode 100644 index 000000000..af8b45001 --- /dev/null +++ b/apache/templates/vhost/_rewrite.erb @@ -0,0 +1,43 @@ +<%- if @rewrites -%> + ## Rewrite rules + RewriteEngine On + <%- if @rewrite_base -%> + RewriteBase <%= @rewrite_base %> + <%- end -%> + + <%- [@rewrites].flatten.compact.each do |rewrite_details| -%> + <%- if rewrite_details['comment'] -%> + #<%= rewrite_details['comment'] %> + <%- end -%> + <%- if rewrite_details['rewrite_base'] -%> + RewriteBase <%= rewrite_details['rewrite_base'] %> + <%- end -%> + <%- if rewrite_details['rewrite_cond'] -%> + <%- Array(rewrite_details['rewrite_cond']).each do |commands| -%> + <%- Array(commands).each do |command| -%> + RewriteCond <%= command %> + <%- end -%> + <%- end -%> + <%- end -%> + <%- Array(rewrite_details['rewrite_rule']).each do |commands| -%> + <%- Array(commands).each do |command| -%> + RewriteRule <%= command %> + <%- end -%> + + <%- end -%> + <%- end -%> +<%- end -%> +<%# reverse compatibility %> +<% if @rewrite_rule and !@rewrites -%> + ## Rewrite rules + RewriteEngine On +<% if @rewrite_base -%> + RewriteBase <%= @rewrite_base %> +<% end -%> +<% if @rewrite_cond -%> +<% Array(@rewrite_cond).each do |cond| -%> + RewriteCond <%= cond %> +<% end -%> +<% end -%> + RewriteRule <%= @rewrite_rule %> +<%- end -%> diff --git a/apache/templates/vhost/_scriptalias.erb b/apache/templates/vhost/_scriptalias.erb new file mode 100644 index 000000000..5a757f617 --- /dev/null +++ b/apache/templates/vhost/_scriptalias.erb @@ -0,0 +1,24 @@ +<%- if @scriptaliases.is_a?(Array) -%> +<%- aliases = @scriptaliases -%> +<%- elsif @scriptaliases.is_a?(Hash) -%> +<%- aliases = [@scriptaliases] -%> +<%- else -%> +<%- # Nothing to do with any other data type -%> +<%- aliases = [] -%> +<%- end -%> +<%- if @scriptalias or !aliases.empty? -%> + ## Script alias directives +<%# Combine scriptalais and scriptaliases into a single data structure -%> +<%# for backward compatibility and ease of implementation -%> +<%- aliases << { 'alias' => '/cgi-bin/', 'path' => @scriptalias } if @scriptalias -%> +<%- aliases.flatten.compact! -%> +<%- aliases.each do |salias| -%> + <%- if salias["path"] != '' -%> + <%- if salias["alias"] and salias["alias"] != '' -%> + ScriptAlias <%= salias['alias'] %> "<%= salias['path'] %>" + <%- elsif salias["aliasmatch"] and salias["aliasmatch"] != '' -%> + ScriptAliasMatch <%= salias['aliasmatch'] %> "<%= salias['path'] %>" + <%- end -%> + <%- end -%> +<%- end -%> +<%- end -%> diff --git a/apache/templates/vhost/_serveralias.erb b/apache/templates/vhost/_serveralias.erb new file mode 100644 index 000000000..278b6ddc5 --- /dev/null +++ b/apache/templates/vhost/_serveralias.erb @@ -0,0 +1,7 @@ +<% if @serveraliases and ! @serveraliases.empty? -%> + + ## Server aliases +<% Array(@serveraliases).each do |serveralias| -%> + ServerAlias <%= serveralias %> +<% end -%> +<% end -%> diff --git a/apache/templates/vhost/_setenv.erb b/apache/templates/vhost/_setenv.erb new file mode 100644 index 000000000..d5f9ea845 --- /dev/null +++ b/apache/templates/vhost/_setenv.erb @@ -0,0 +1,12 @@ +<% if @setenv and ! @setenv.empty? -%> + + ## SetEnv/SetEnvIf for environment variables +<% Array(@setenv).each do |envvar| -%> + SetEnv <%= envvar %> +<% end -%> +<% end -%> +<% if @setenvif and ! @setenvif.empty? -%> +<% Array(@setenvif).each do |envifvar| -%> + SetEnvIf <%= envifvar %> +<% end -%> +<% end -%> diff --git a/apache/templates/vhost/_ssl.erb b/apache/templates/vhost/_ssl.erb new file mode 100644 index 000000000..03c78ef42 --- /dev/null +++ b/apache/templates/vhost/_ssl.erb @@ -0,0 +1,41 @@ +<% if @ssl -%> + + ## SSL directives + SSLEngine on + SSLCertificateFile "<%= @ssl_cert %>" + SSLCertificateKeyFile "<%= @ssl_key %>" +<% if @ssl_chain -%> + SSLCertificateChainFile "<%= @ssl_chain %>" +<% end -%> + SSLCACertificatePath "<%= @ssl_certs_dir %>" +<% if @ssl_ca -%> + SSLCACertificateFile "<%= @ssl_ca %>" +<% end -%> +<% if @ssl_crl_path -%> + SSLCARevocationPath "<%= @ssl_crl_path %>" +<% end -%> +<% if @ssl_crl -%> + SSLCARevocationFile "<%= @ssl_crl %>" +<% end -%> +<% if @ssl_proxyengine -%> + SSLProxyEngine On +<% end -%> +<% if @ssl_protocol -%> + SSLProtocol <%= @ssl_protocol %> +<% end -%> +<% if @ssl_cipher -%> + SSLCipherSuite <%= @ssl_cipher %> +<% end -%> +<% if @ssl_honorcipherorder -%> + SSLHonorCipherOrder <%= @ssl_honorcipherorder %> +<% end -%> +<% if @ssl_verify_client -%> + SSLVerifyClient <%= @ssl_verify_client %> +<% end -%> +<% if @ssl_verify_depth -%> + SSLVerifyDepth <%= @ssl_verify_depth %> +<% end -%> +<% if @ssl_options -%> + SSLOptions <%= Array(@ssl_options).join(' ') %> +<% end -%> +<% end -%> diff --git a/apache/templates/vhost/_suphp.erb b/apache/templates/vhost/_suphp.erb new file mode 100644 index 000000000..938958180 --- /dev/null +++ b/apache/templates/vhost/_suphp.erb @@ -0,0 +1,11 @@ +<% if @suphp_engine == 'on' -%> +<% if @suphp_addhandler -%> + suPHP_AddHandler <%= @suphp_addhandler %> +<% end -%> +<% if @suphp_engine -%> + suPHP_Engine <%= @suphp_engine %> +<% end -%> +<% if @suphp_configpath -%> + suPHP_ConfigPath "<%= @suphp_configpath %>" +<% end -%> +<% end -%> diff --git a/apache/templates/vhost/_wsgi.erb b/apache/templates/vhost/_wsgi.erb new file mode 100644 index 000000000..474c30ff1 --- /dev/null +++ b/apache/templates/vhost/_wsgi.erb @@ -0,0 +1,21 @@ +<% if @wsgi_application_group -%> + WSGIApplicationGroup <%= @wsgi_application_group %> +<% end -%> +<% if @wsgi_daemon_process and @wsgi_daemon_process_options -%> + WSGIDaemonProcess <%= @wsgi_daemon_process %> <%= @wsgi_daemon_process_options.collect { |k,v| "#{k}=#{v}"}.sort.join(' ') %> +<% elsif @wsgi_daemon_process and !@wsgi_daemon_process_options -%> + WSGIDaemonProcess <%= @wsgi_daemon_process %> +<% end -%> +<% if @wsgi_import_script and @wsgi_import_script_options -%> + WSGIImportScript <%= @wsgi_import_script %> <%= @wsgi_import_script_options.collect { |k,v| "#{k}=#{v}"}.sort.join(' ') %> +<% end -%> +<% if @wsgi_process_group -%> + WSGIProcessGroup <%= @wsgi_process_group %> +<% end -%> +<% if @wsgi_script_aliases and ! @wsgi_script_aliases.empty? -%> + <%- @wsgi_script_aliases.each do |a, p| -%> + <%- if a != '' and p != ''-%> + WSGIScriptAlias <%= a %> "<%= p %>" + <%- end -%> + <%- end -%> +<% end -%> diff --git a/apache/tests/apache.pp b/apache/tests/apache.pp new file mode 100644 index 000000000..0d4543564 --- /dev/null +++ b/apache/tests/apache.pp @@ -0,0 +1,6 @@ +include apache +include apache::mod::php +include apache::mod::cgi +include apache::mod::userdir +include apache::mod::disk_cache +include apache::mod::proxy_http diff --git a/apache/tests/dev.pp b/apache/tests/dev.pp new file mode 100644 index 000000000..805ad7e37 --- /dev/null +++ b/apache/tests/dev.pp @@ -0,0 +1 @@ +include apache::dev diff --git a/apache/tests/init.pp b/apache/tests/init.pp new file mode 100644 index 000000000..b3f9f13aa --- /dev/null +++ b/apache/tests/init.pp @@ -0,0 +1 @@ +include apache diff --git a/apache/tests/mod_load_params.pp b/apache/tests/mod_load_params.pp new file mode 100644 index 000000000..0e84c5efb --- /dev/null +++ b/apache/tests/mod_load_params.pp @@ -0,0 +1,11 @@ +# Tests the path and identifier parameters for the apache::mod class + +# Base class for clarity: +class { 'apache': } + + +# Exaple parameter usage: +apache::mod { 'testmod': + path => '/usr/some/path/mod_testmod.so', + id => 'testmod_custom_name', +} diff --git a/apache/tests/mods.pp b/apache/tests/mods.pp new file mode 100644 index 000000000..59362bd9a --- /dev/null +++ b/apache/tests/mods.pp @@ -0,0 +1,9 @@ +## Default mods + +# Base class. Declares default vhost on port 80 and default ssl +# vhost on port 443 listening on all interfaces and serving +# $apache::docroot, and declaring our default set of modules. +class { 'apache': + default_mods => true, +} + diff --git a/apache/tests/mods_custom.pp b/apache/tests/mods_custom.pp new file mode 100644 index 000000000..0ae699c73 --- /dev/null +++ b/apache/tests/mods_custom.pp @@ -0,0 +1,16 @@ +## custom mods + +# Base class. Declares default vhost on port 80 and default ssl +# vhost on port 443 listening on all interfaces and serving +# $apache::docroot, and declaring a custom set of modules. +class { 'apache': + default_mods => [ + 'info', + 'alias', + 'mime', + 'env', + 'setenv', + 'expires', + ], +} + diff --git a/apache/tests/php.pp b/apache/tests/php.pp new file mode 100644 index 000000000..1d926bfb4 --- /dev/null +++ b/apache/tests/php.pp @@ -0,0 +1,4 @@ +class { 'apache': + mpm_module => 'prefork', +} +include apache::mod::php diff --git a/apache/tests/vhost.pp b/apache/tests/vhost.pp new file mode 100644 index 000000000..f0d3f58e4 --- /dev/null +++ b/apache/tests/vhost.pp @@ -0,0 +1,237 @@ +## Default vhosts, and custom vhosts +# NB: Please see the other vhost_*.pp example files for further +# examples. + +# Base class. Declares default vhost on port 80 and default ssl +# vhost on port 443 listening on all interfaces and serving +# $apache::docroot +class { 'apache': } + +# Most basic vhost +apache::vhost { 'first.example.com': + port => '80', + docroot => '/var/www/first', +} + +# Vhost with different docroot owner/group +apache::vhost { 'second.example.com': + port => '80', + docroot => '/var/www/second', + docroot_owner => 'third', + docroot_group => 'third', +} + +# Vhost with serveradmin +apache::vhost { 'third.example.com': + port => '80', + docroot => '/var/www/third', + serveradmin => 'admin@example.com', +} + +# Vhost with ssl (uses default ssl certs) +apache::vhost { 'ssl.example.com': + port => '443', + docroot => '/var/www/ssl', + ssl => true, +} + +# Vhost with ssl and specific ssl certs +apache::vhost { 'fourth.example.com': + port => '443', + docroot => '/var/www/fourth', + ssl => true, + ssl_cert => '/etc/ssl/fourth.example.com.cert', + ssl_key => '/etc/ssl/fourth.example.com.key', +} + +# Vhost with english title and servername parameter +apache::vhost { 'The fifth vhost': + servername => 'fifth.example.com', + port => '80', + docroot => '/var/www/fifth', +} + +# Vhost with server aliases +apache::vhost { 'sixth.example.com': + serveraliases => [ + 'sixth.example.org', + 'sixth.example.net', + ], + port => '80', + docroot => '/var/www/fifth', +} + +# Vhost with alternate options +apache::vhost { 'seventh.example.com': + port => '80', + docroot => '/var/www/seventh', + options => [ + 'Indexes', + 'MultiViews', + ], +} + +# Vhost with AllowOverride for .htaccess +apache::vhost { 'eighth.example.com': + port => '80', + docroot => '/var/www/eighth', + override => 'All', +} + +# Vhost with access and error logs disabled +apache::vhost { 'ninth.example.com': + port => '80', + docroot => '/var/www/ninth', + access_log => false, + error_log => false, +} + +# Vhost with custom access and error logs and logroot +apache::vhost { 'tenth.example.com': + port => '80', + docroot => '/var/www/tenth', + access_log_file => 'tenth_vhost.log', + error_log_file => 'tenth_vhost_error.log', + logroot => '/var/log', +} + +# Vhost with a cgi-bin +apache::vhost { 'eleventh.example.com': + port => '80', + docroot => '/var/www/eleventh', + scriptalias => '/usr/lib/cgi-bin', +} + +# Vhost with a proxypass configuration +apache::vhost { 'twelfth.example.com': + port => '80', + docroot => '/var/www/twelfth', + proxy_dest => 'http://internal.example.com:8080/twelfth', + no_proxy_uris => ['/login','/logout'], +} + +# Vhost to redirect /login and /logout +apache::vhost { 'thirteenth.example.com': + port => '80', + docroot => '/var/www/thirteenth', + redirect_source => [ + '/login', + '/logout', + ], + redirect_dest => [ + 'http://10.0.0.10/login', + 'http://10.0.0.10/logout', + ], +} + +# Vhost to permamently redirect +apache::vhost { 'fourteenth.example.com': + port => '80', + docroot => '/var/www/fourteenth', + redirect_source => '/blog', + redirect_dest => 'http://blog.example.com', + redirect_status => 'permanent', +} + +# Vhost with a rack configuration +apache::vhost { 'fifteenth.example.com': + port => '80', + docroot => '/var/www/fifteenth', + rack_base_uris => ['/rackapp1', '/rackapp2'], +} + +# Vhost to redirect non-ssl to ssl +apache::vhost { 'sixteenth.example.com non-ssl': + servername => 'sixteenth.example.com', + port => '80', + docroot => '/var/www/sixteenth', + rewrites => [ + { + comment => 'redirect non-SSL traffic to SSL site', + rewrite_cond => ['%{HTTPS} off'], + rewrite_rule => ['(.*) https://%{HTTPS_HOST}%{REQUEST_URI}'], + } + ] +} +apache::vhost { 'sixteenth.example.com ssl': + servername => 'sixteenth.example.com', + port => '443', + docroot => '/var/www/sixteenth', + ssl => true, +} + +# Vhost to redirect non-ssl to ssl using old rewrite method +apache::vhost { 'sixteenth.example.com non-ssl old rewrite': + servername => 'sixteenth.example.com', + port => '80', + docroot => '/var/www/sixteenth', + rewrite_cond => '%{HTTPS} off', + rewrite_rule => '(.*) https://%{HTTPS_HOST}%{REQUEST_URI}', +} +apache::vhost { 'sixteenth.example.com ssl old rewrite': + servername => 'sixteenth.example.com', + port => '443', + docroot => '/var/www/sixteenth', + ssl => true, +} + +# Vhost to block repository files +apache::vhost { 'seventeenth.example.com': + port => '80', + docroot => '/var/www/seventeenth', + block => 'scm', +} + +# Vhost with special environment variables +apache::vhost { 'eighteenth.example.com': + port => '80', + docroot => '/var/www/eighteenth', + setenv => ['SPECIAL_PATH /foo/bin','KILROY was_here'], +} + +apache::vhost { 'nineteenth.example.com': + port => '80', + docroot => '/var/www/nineteenth', + setenvif => 'Host "^([^\.]*)\.website\.com$" CLIENT_NAME=$1', +} + +# Vhost with additional include files +apache::vhost { 'twentyieth.example.com': + port => '80', + docroot => '/var/www/twelfth', + additional_includes => ['/tmp/proxy_group_a','/tmp/proxy_group_b'], +} + +# Vhost with alias for subdomain mapped to same named directory +# http://example.com.loc => /var/www/example.com +apache::vhost { 'subdomain.loc': + vhost_name => '*', + port => '80', + virtual_docroot => '/var/www/%-2+', + docroot => '/var/www', + serveraliases => ['*.loc',], +} + +# Vhost with SSLProtocol,SSLCipherSuite, SSLHonorCipherOrder +apache::vhost { 'securedomain.com': + priority => '10', + vhost_name => 'www.securedomain.com', + port => '443', + docroot => '/var/www/secure', + ssl => true, + ssl_cert => '/etc/ssl/securedomain.cert', + ssl_key => '/etc/ssl/securedomain.key', + ssl_chain => '/etc/ssl/securedomain.crt', + ssl_protocol => '-ALL +SSLv3 +TLSv1', + ssl_cipher => 'ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM', + ssl_honorcipherorder => 'On', + add_listen => false, +} + +# Vhost with access log environment variables writing control +apache::vhost { 'twentyfirst.example.com': + port => '80', + docroot => '/var/www/twentyfirst', + access_log_env_var => 'admin', +} + diff --git a/apache/tests/vhost_directories.pp b/apache/tests/vhost_directories.pp new file mode 100644 index 000000000..b8953ee32 --- /dev/null +++ b/apache/tests/vhost_directories.pp @@ -0,0 +1,44 @@ +# Base class. Declares default vhost on port 80 and default ssl +# vhost on port 443 listening on all interfaces and serving +# $apache::docroot +class { 'apache': } + +# Example from README adapted. +apache::vhost { 'readme.example.net': + docroot => '/var/www/readme', + directories => [ + { + 'path' => '/var/www/readme', + 'ServerTokens' => 'prod' , + }, + { + 'path' => '/usr/share/empty', + 'allow' => 'from all', + }, + ], +} + +# location test +apache::vhost { 'location.example.net': + docroot => '/var/www/location', + directories => [ + { + 'path' => '/location', + 'provider' => 'location', + 'ServerTokens' => 'prod' + }, + ], +} + +# files test, curedly disable access to accidental backup files. +apache::vhost { 'files.example.net': + docroot => '/var/www/files', + directories => [ + { + 'path' => '(\.swp|\.bak|~)$', + 'provider' => 'filesmatch', + 'deny' => 'from all' + }, + ], +} + diff --git a/apache/tests/vhost_ip_based.pp b/apache/tests/vhost_ip_based.pp new file mode 100644 index 000000000..dc0fa4f33 --- /dev/null +++ b/apache/tests/vhost_ip_based.pp @@ -0,0 +1,25 @@ +## IP-based vhosts on any listen port +# IP-based vhosts respond to requests on specific IP addresses. + +# Base class. Turn off the default vhosts; we will be declaring +# all vhosts below. +class { 'apache': + default_vhost => false, +} + +# Listen on port 80 and 81; required because the following vhosts +# are not declared with a port parameter. +apache::listen { '80': } +apache::listen { '81': } + +# IP-based vhosts +apache::vhost { 'first.example.com': + ip => '10.0.0.10', + docroot => '/var/www/first', + ip_based => true, +} +apache::vhost { 'second.example.com': + ip => '10.0.0.11', + docroot => '/var/www/second', + ip_based => true, +} diff --git a/apache/tests/vhost_ssl.pp b/apache/tests/vhost_ssl.pp new file mode 100644 index 000000000..8e7a2b279 --- /dev/null +++ b/apache/tests/vhost_ssl.pp @@ -0,0 +1,23 @@ +## SSL-enabled vhosts +# SSL-enabled vhosts respond only to HTTPS queries. + +# Base class. Turn off the default vhosts; we will be declaring +# all vhosts below. +class { 'apache': + default_vhost => false, +} + +# Non-ssl vhost +apache::vhost { 'first.example.com non-ssl': + servername => 'first.example.com', + port => '80', + docroot => '/var/www/first', +} + +# SSL vhost at the same domain +apache::vhost { 'first.example.com ssl': + servername => 'first.example.com', + port => '443', + docroot => '/var/www/first', + ssl => true, +} diff --git a/apache/tests/vhosts_without_listen.pp b/apache/tests/vhosts_without_listen.pp new file mode 100644 index 000000000..e7d6cc036 --- /dev/null +++ b/apache/tests/vhosts_without_listen.pp @@ -0,0 +1,53 @@ +## Declare ip-based and name-based vhosts +# Mixing Name-based vhost with IP-specific vhosts requires `add_listen => +# 'false'` on the non-IP vhosts + +# Base class. Turn off the default vhosts; we will be declaring +# all vhosts below. +class { 'apache': + default_vhost => false, +} + + +# Add two an IP-based vhost on 10.0.0.10, ssl and non-ssl +apache::vhost { 'The first IP-based vhost, non-ssl': + servername => 'first.example.com', + ip => '10.0.0.10', + port => '80', + ip_based => true, + docroot => '/var/www/first', +} +apache::vhost { 'The first IP-based vhost, ssl': + servername => 'first.example.com', + ip => '10.0.0.10', + port => '443', + ip_based => true, + docroot => '/var/www/first-ssl', + ssl => true, +} + +# Two name-based vhost listening on 10.0.0.20 +apache::vhost { 'second.example.com': + ip => '10.0.0.20', + port => '80', + docroot => '/var/www/second', +} +apache::vhost { 'third.example.com': + ip => '10.0.0.20', + port => '80', + docroot => '/var/www/third', +} + +# Two name-based vhosts without IPs specified, so that they will answer on either 10.0.0.10 or 10.0.0.20 . It is requried to declare +# `add_listen => 'false'` to disable declaring "Listen 80" which will conflict +# with the IP-based preceeding vhosts. +apache::vhost { 'fourth.example.com': + port => '80', + docroot => '/var/www/fourth', + add_listen => false, +} +apache::vhost { 'fifth.example.com': + port => '80', + docroot => '/var/www/fifth', + add_listen => false, +} diff --git a/ceilometer b/ceilometer deleted file mode 160000 index 08fc9d915..000000000 --- a/ceilometer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 08fc9d9159cd9eb0830d550abb1058bc2b9b5759 diff --git a/ceilometer/.fixtures.yml b/ceilometer/.fixtures.yml new file mode 100644 index 000000000..cf0f5e521 --- /dev/null +++ b/ceilometer/.fixtures.yml @@ -0,0 +1,12 @@ +fixtures: + repositories: + 'inifile': 'git://github.com/puppetlabs/puppetlabs-inifile' + 'keystone': 'git://github.com/stackforge/puppet-keystone.git' + 'mysql': + repo: 'git://github.com/puppetlabs/puppetlabs-mysql.git' + ref: 'origin/2.2.x' + 'nova': 'git://github.com/stackforge/puppet-nova.git' + 'openstacklib': 'git://github.com/stackforge/puppet-openstacklib.git' + 'stdlib': 'git://github.com/puppetlabs/puppetlabs-stdlib.git' + symlinks: + 'ceilometer': "#{source_dir}" diff --git a/ceilometer/.gitignore b/ceilometer/.gitignore new file mode 100644 index 000000000..d4a93a066 --- /dev/null +++ b/ceilometer/.gitignore @@ -0,0 +1,4 @@ +*.swp +spec/fixtures/modules/* +pkg +Gemfile.lock diff --git a/ceilometer/.gitreview b/ceilometer/.gitreview new file mode 100644 index 000000000..721c4d8a5 --- /dev/null +++ b/ceilometer/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=stackforge/puppet-ceilometer.git diff --git a/ceilometer/.travis.yml b/ceilometer/.travis.yml new file mode 100644 index 000000000..63844457d --- /dev/null +++ b/ceilometer/.travis.yml @@ -0,0 +1,28 @@ +language: ruby +bundler_args: --without development +before_script: + - echo $PUPPET_GEM_VERSION | grep '2.6' && git clone git://github.com/puppetlabs/puppetlabs-create_resources.git spec/fixtures/modules/create_resources || true +script: "bundle exec rake spec SPEC_OPTS='--format documentation'" +rvm: + - 1.8.7 + - 1.9.3 + - ruby-head +env: + - PUPPET_GEM_VERSION="~> 2.6" + - PUPPET_GEM_VERSION="~> 2.7" + - PUPPET_GEM_VERSION="~> 3.0" + - PUPPET_GEM_VERSION="~> 3.1" +matrix: + allow_failures: + - rvm: ruby-head + exclude: + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 2.7" + - rvm: ruby-head + env: PUPPET_GEM_VERSION="~> 2.7" + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 2.6" + - rvm: ruby-head + env: PUPPET_GEM_VERSION="~> 2.6" +notifications: + email: false diff --git a/ceilometer/Gemfile b/ceilometer/Gemfile new file mode 100644 index 000000000..d965fa900 --- /dev/null +++ b/ceilometer/Gemfile @@ -0,0 +1,18 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'puppetlabs_spec_helper', :require => false + gem 'puppet-lint', '~> 0.3.2' + gem 'rake', '10.1.1' + gem 'rspec', '< 2.99' + gem 'json' + gem 'webmock' +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/ceilometer/LICENSE b/ceilometer/LICENSE new file mode 100644 index 000000000..dd5b3a58a --- /dev/null +++ b/ceilometer/LICENSE @@ -0,0 +1,174 @@ + 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. diff --git a/ceilometer/Modulefile b/ceilometer/Modulefile new file mode 100644 index 000000000..e7b7fb049 --- /dev/null +++ b/ceilometer/Modulefile @@ -0,0 +1,13 @@ +name 'puppetlabs-ceilometer' +version '4.0.0' +author 'eNovance and StackForge Contributors' +license 'Apache License 2.0' +summary 'Puppet module for OpenStack Ceilometer' +description 'Installs and configures OpenStack Ceilometer (Telemetry).' +project_page 'https://launchpad.net/puppet-ceilometer' +source 'https://github.com/stackforge/puppet-ceilometer' + +dependency 'puppetlabs/inifile', '>=1.0.0 <2.0.0' +dependency 'puppetlabs/keystone', '>=4.0.0 <5.0.0' +dependency 'puppetlabs/stdlib', '>=4.0.0 < 5.0.0' +dependency 'stackforge/openstacklib', '>=5.0.0' diff --git a/ceilometer/README.md b/ceilometer/README.md new file mode 100644 index 000000000..afe09c5ac --- /dev/null +++ b/ceilometer/README.md @@ -0,0 +1,128 @@ +Ceilometer +========== + +4.0.0 - 2014.1.0 - Icehouse + +#### Table of Contents + +1. [Overview - What is the ceilometer module?](#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with ceilometer](#setup) +4. [Implementation - An under-the-hood peek at what the module is doing](#implementation) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) +7. [Contributors - Those with commits](#contributors) +8. [Release Notes - Notes on the most recent updates to the module](#release-notes) + +Overview +-------- + +The ceilometer module is part of [Stackforge](https://github.com/stackforge), an effort by the +OpenStack infrastructure team to provice continuous integration testing and code review for +OpenStack and OpenStack community projects not part of the core software. The module itself +is used to flexibly configure and manage the metering service for OpenStack. + +Module Description +------------------ + +The ceilometer module is an attempt to make Puppet capable of managing the entirety of ceilometer. +This includes manifests to provision the ceilometer api, agents, and database stores. A +ceilometer_config type is supplied to assist in the manipulation of configuration files. + +Setup +----- + +**What the ceilometer module affects** + +* ceilometer, the metering service for OpenStack + +### Installing ceilometer + + example% puppet module install puppetlabs/ceilometer + +### Beginning with ceilometer + +Implementation +-------------- + +### ceilometer + +ceilometer is a combination of Puppet manifests and Ruby code to deliver configuration and +extra functionality through types and providers. + +Limitations +----------- + +* The ceilometer modules have only been tested on RedHat and Ubuntu family systems. + +Development +----------- + +Developer documentation for the entire puppet-openstack project + +* https://wiki.openstack.org/wiki/Puppet-openstack#Developer_documentation + +Contributors +------------ + +* https://github.com/stackforge/puppet-ceilometer/graphs/contributors + +This is the ceilometer module. + +Release Notes +------------- + +** 4.0.0 ** + +* Stable Icehouse release. +* Added ability to override notification topics. +* Implemented notification agent service. +* Fixed region name configuration. +* Fixed ensure packages bug. +* Added support for puppetlabs-mysql 2.2 and greater. +* Fixed MySQL grant call. +* Introduced ceilometer::config to handle additional custom options. + +** 3.1.1 ** + +* Removed enforcement of glance_control_exchange. +* Fixed user reference in db.pp. +* Allow db fields configuration without need for dbsync for better replicaset support. +* Fixed alarm package parameters Debian/Ubuntu. + + +** 3.1.0 ** + +* Fixed package ceilometer-alarm type error on Debian. +* Remove log_dir from params and make logs configurable in init. +* Removed glance_notifications from notification_topic. +* Don't match commented [DEFAULT] section. + +** 3.0.0 ** + +* Initial release of the puppet-ceilometer module. + + +License +-------- + +Apache License 2.0 + + Copyright 2012 eNovance + + 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. + +Contact +------- + +techs@enovance.com diff --git a/ceilometer/Rakefile b/ceilometer/Rakefile new file mode 100644 index 000000000..4c2b2ed07 --- /dev/null +++ b/ceilometer/Rakefile @@ -0,0 +1,6 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings = true +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_parameter_defaults') diff --git a/ceilometer/examples/site.pp b/ceilometer/examples/site.pp new file mode 100644 index 000000000..b8dd7233f --- /dev/null +++ b/ceilometer/examples/site.pp @@ -0,0 +1,70 @@ +node default { + Exec { + path => ['/usr/bin', '/bin', '/usr/sbin', '/sbin'] + } + + # First, install a mysql server + class { 'mysql::server': } + # And create the database + class { 'ceilometer::db::mysql': + password => 'ceilometer', + } + + # Add the base ceilometer class & parameters + # This class is required by ceilometer agents & api classes + # The metering_secret parameter is mandatory + class { 'ceilometer': + metering_secret => 'darksecret' + } + + # Configure the ceilometer database + # Only needed if ceilometer::agent::central or ceilometer::api are declared + class { 'ceilometer::db': + } + + # Configure ceilometer database with mongodb + + # class { 'ceilometer::db': + # database_connection => 'mongodb://localhost:27017/ceilometer', + # require => Class['mongodb'], + # } + + # Install the ceilometer-api service + # The keystone_password parameter is mandatory + class { 'ceilometer::api': + keystone_password => 'tralalayouyou' + } + + # Set common auth parameters used by all agents (compute/central) + class { 'ceilometer::agent::auth': + auth_url => 'http://localhost:35357/v2.0', + auth_password => 'tralalerotralala' + } + + # Install compute agent + # default: enable + class { 'ceilometer::agent::compute': + } + + # Install central agent + class { 'ceilometer::agent::central': + } + + # Install alarm notifier + class { 'ceilometer::alarm::notifier': + } + + # Install alarm evaluator + class { 'ceilometer::alarm::evaluator': + } + + # Purge 1 month old meters + class { 'ceilometer::expirer': + time_to_live => '2592000' + } + + # Install notification agent + class { 'ceilometer::agent::notification': + } + +} diff --git a/ceilometer/lib/puppet/provider/ceilometer_config/ini_setting.rb b/ceilometer/lib/puppet/provider/ceilometer_config/ini_setting.rb new file mode 100644 index 000000000..10ae8fb0b --- /dev/null +++ b/ceilometer/lib/puppet/provider/ceilometer_config/ini_setting.rb @@ -0,0 +1,22 @@ +Puppet::Type.type(:ceilometer_config).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/ceilometer/ceilometer.conf' + end + +end diff --git a/ceilometer/lib/puppet/provider/file_line_after/ruby.rb b/ceilometer/lib/puppet/provider/file_line_after/ruby.rb new file mode 100644 index 000000000..0d7f287c9 --- /dev/null +++ b/ceilometer/lib/puppet/provider/file_line_after/ruby.rb @@ -0,0 +1,83 @@ +Puppet::Type.type(:file_line_after).provide(:ruby) do + def exists? + lines.find do |line| + line.chomp == resource[:line].chomp + end + end + + def create + if resource[:match] + handle_create_with_match + elsif resource[:after] + handle_create_with_after + else + append_line + end + end + + def destroy + local_lines = lines + File.open(resource[:path],'w') do |fh| + fh.write(local_lines.reject{|l| l.chomp == resource[:line] }.join('')) + end + end + + private + def lines + # If this type is ever used with very large files, we should + # write this in a different way, using a temp + # file; for now assuming that this type is only used on + # small-ish config files that can fit into memory without + # too much trouble. + @lines ||= File.readlines(resource[:path]) + end + + def handle_create_with_match() + regex = resource[:match] ? Regexp.new(resource[:match]) : nil + match_count = lines.select { |l| regex.match(l) }.size + if match_count > 1 && resource[:multiple].to_s != 'true' + raise Puppet::Error, "More than one line in file '#{resource[:path]}' matches pattern '#{resource[:match]}'" + end + File.open(resource[:path], 'w') do |fh| + lines.each do |l| + fh.puts(regex.match(l) ? resource[:line] : l) + end + + if (match_count == 0) + fh.puts(resource[:line]) + end + end + end + + def handle_create_with_after + regex = Regexp.new(resource[:after]) + + count = lines.count {|l| l.match(regex)} + + case count + when 1 # find the line to put our line after + File.open(resource[:path], 'w') do |fh| + lines.each do |l| + fh.puts(l) + if regex.match(l) then + fh.puts(resource[:line]) + end + end + end + when 0 # append the line to the end of the file + append_line + else + raise Puppet::Error, "#{count} lines match pattern '#{resource[:after]}' in file '#{resource[:path]}'. One or no line must match the pattern." + end + end + + ## + # append the line to the file. + # + # @api private + def append_line + File.open(resource[:path], 'a') do |fh| + fh.puts resource[:line] + end + end +end diff --git a/ceilometer/lib/puppet/type/ceilometer_config.rb b/ceilometer/lib/puppet/type/ceilometer_config.rb new file mode 100644 index 000000000..77215b251 --- /dev/null +++ b/ceilometer/lib/puppet/type/ceilometer_config.rb @@ -0,0 +1,44 @@ +Puppet::Type.newtype(:ceilometer_config) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from ceilometer.conf' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + newvalues(/^[\S ]*$/) + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end + +end diff --git a/ceilometer/lib/puppet/type/file_line_after.rb b/ceilometer/lib/puppet/type/file_line_after.rb new file mode 100644 index 000000000..d71d7c298 --- /dev/null +++ b/ceilometer/lib/puppet/type/file_line_after.rb @@ -0,0 +1,79 @@ +Puppet::Type.newtype(:file_line_after) do + + desc <<-EOT + Ensures that a given line is contained within a file. The implementation + matches the full line, including whitespace at the beginning and end. If + the line is not contained in the given file, Puppet will add the line to + ensure the desired state. Multiple resources may be declared to manage + multiple lines in the same file. + + Example: + + file_line_after { 'sudo_rule': + path => '/etc/sudoers', + line => '%sudo ALL=(ALL) ALL', + } + file_line_after { 'sudo_rule_nopw': + path => '/etc/sudoers', + line => '%sudonopw ALL=(ALL) NOPASSWD: ALL', + } + + In this example, Puppet will ensure both of the specified lines are + contained in the file /etc/sudoers. + + EOT + + ensurable do + defaultvalues + defaultto :present + end + + newparam(:name, :namevar => true) do + desc 'An arbitrary name used as the identity of the resource.' + end + + newparam(:match) do + desc 'An optional regular expression to run against existing lines in the file;\n' + + 'if a match is found, we replace that line rather than adding a new line.' + end + + newparam(:multiple) do + desc 'An optional value to determine if match can change multiple lines.' + newvalues(true, false) + end + + newparam(:after) do + desc 'An optional value used to specify the line after which we will add any new lines. (Existing lines are added in place)' + end + + newparam(:line) do + desc 'The line to be appended to the file located by the path parameter.' + end + + newparam(:path) do + desc 'The file Puppet will ensure contains the line specified by the line parameter.' + validate do |value| + unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) + raise(Puppet::Error, "File paths must be fully qualified, not '#{value}'") + end + end + end + + # Autorequire the file resource if it's being managed + autorequire(:file) do + self[:path] + end + + validate do + unless self[:line] and self[:path] + raise(Puppet::Error, "Both line and path are required attributes") + end + + if (self[:match]) + unless Regexp.new(self[:match]).match(self[:line]) + raise(Puppet::Error, "When providing a 'match' parameter, the value must be a regex that matches against the value of your 'line' parameter") + end + end + + end +end diff --git a/ceilometer/manifests/agent/auth.pp b/ceilometer/manifests/agent/auth.pp new file mode 100644 index 000000000..2b6f536e2 --- /dev/null +++ b/ceilometer/manifests/agent/auth.pp @@ -0,0 +1,62 @@ +# The ceilometer::agent::auth class helps configure common +# auth settings for the agents. +# +# == Parameters +# [*auth_url*] +# the keystone public endpoint +# Optional. Defaults to 'http://localhost:5000/v2.0' +# +# [*auth_region*] +# the keystone region of this node +# Optional. Defaults to 'RegionOne' +# +# [*auth_user*] +# the keystone user for ceilometer services +# Optional. Defaults to 'ceilometer' +# +# [*auth_password*] +# the keystone password for ceilometer services +# Required. +# +# [*auth_tenant_name*] +# the keystone tenant name for ceilometer services +# Optional. Defaults to 'services' +# +# [*auth_tenant_id*] +# the keystone tenant id for ceilometer services. +# Optional. Defaults to empty. +# +# [*auth_cacert*] +# Certificate chain for SSL validation. Optional; Defaults to 'None' +# +class ceilometer::agent::auth ( + $auth_password, + $auth_url = 'http://localhost:5000/v2.0', + $auth_region = 'RegionOne', + $auth_user = 'ceilometer', + $auth_tenant_name = 'services', + $auth_tenant_id = '', + $auth_cacert = undef, +) { + + if ! $auth_cacert { + ceilometer_config { 'service_credentials/os_cacert': ensure => absent } + } else { + ceilometer_config { 'service_credentials/os_cacert': value => $auth_cacert } + } + + ceilometer_config { + 'service_credentials/os_auth_url' : value => $auth_url; + 'service_credentials/os_region_name' : value => $auth_region; + 'service_credentials/os_username' : value => $auth_user; + 'service_credentials/os_password' : value => $auth_password, secret => true; + 'service_credentials/os_tenant_name' : value => $auth_tenant_name; + } + + if ($auth_tenant_id != '') { + ceilometer_config { + 'service_credentials/os_tenant_id' : value => $auth_tenant_id; + } + } + +} diff --git a/ceilometer/manifests/agent/central.pp b/ceilometer/manifests/agent/central.pp new file mode 100644 index 000000000..7c17d8c11 --- /dev/null +++ b/ceilometer/manifests/agent/central.pp @@ -0,0 +1,44 @@ +# Installs/configures the ceilometer central agent +# +# == Parameters +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true. +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +class ceilometer::agent::central ( + $manage_service = true, + $enabled = true, +) { + + include ceilometer::params + + Ceilometer_config<||> ~> Service['ceilometer-agent-central'] + + Package['ceilometer-agent-central'] -> Service['ceilometer-agent-central'] + package { 'ceilometer-agent-central': + ensure => installed, + name => $::ceilometer::params::agent_central_package_name, + } + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + Package['ceilometer-common'] -> Service['ceilometer-agent-central'] + service { 'ceilometer-agent-central': + ensure => $service_ensure, + name => $::ceilometer::params::agent_central_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + } + +} diff --git a/ceilometer/manifests/agent/compute.pp b/ceilometer/manifests/agent/compute.pp new file mode 100644 index 000000000..31048360b --- /dev/null +++ b/ceilometer/manifests/agent/compute.pp @@ -0,0 +1,79 @@ +# The ceilometer::agent::compute class installs the ceilometer compute agent +# Include this class on all nova compute nodes +# +# == Parameters +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true. +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +class ceilometer::agent::compute ( + $manage_service = true, + $enabled = true, +) inherits ceilometer { + + include ceilometer::params + + Ceilometer_config<||> ~> Service['ceilometer-agent-compute'] + + Package['ceilometer-agent-compute'] -> Service['ceilometer-agent-compute'] + package { 'ceilometer-agent-compute': + ensure => installed, + name => $::ceilometer::params::agent_compute_package_name, + } + + if $::ceilometer::params::libvirt_group { + User['ceilometer'] { + groups => ['nova', $::ceilometer::params::libvirt_group] + } + } else { + User['ceilometer'] { + groups => ['nova'] + } + } + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + Package['nova-common'] -> Package['ceilometer-common'] -> Service['ceilometer-agent-compute'] + service { 'ceilometer-agent-compute': + ensure => $service_ensure, + name => $::ceilometer::params::agent_compute_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + } + + #NOTE(dprince): This is using a custom (inline) file_line provider + # until this lands upstream: + # https://github.com/puppetlabs/puppetlabs-stdlib/pull/174 + Nova_config<| |> { + before +> File_line_after[ + 'nova-notification-driver-common', + 'nova-notification-driver-ceilometer' + ], + } + + file_line_after { + 'nova-notification-driver-common': + line => + 'notification_driver=nova.openstack.common.notifier.rpc_notifier', + path => '/etc/nova/nova.conf', + after => '^\s*\[DEFAULT\]', + notify => Service['nova-compute']; + 'nova-notification-driver-ceilometer': + line => 'notification_driver=ceilometer.compute.nova_notifier', + path => '/etc/nova/nova.conf', + after => '^\s*\[DEFAULT\]', + notify => Service['nova-compute']; + } + +} diff --git a/ceilometer/manifests/agent/notification.pp b/ceilometer/manifests/agent/notification.pp new file mode 100644 index 000000000..f8c2631c8 --- /dev/null +++ b/ceilometer/manifests/agent/notification.pp @@ -0,0 +1,79 @@ +# +# Copyright (C) 2014 eNovance SAS +# +# Author: Emilien Macchi +# +# 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: ceilometer::agent::notification +# +# Configure the ceilometer notification agent. +# This configures the plugin for the API server, but does nothing +# about configuring the agents that must also run and share a config +# file with the OVS plugin if both are on the same machine. +# +# === Parameters +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true. +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +# [*ack_on_event_error*] +# (optional) Acknowledge message when event persistence fails. +# Defaults to true +# +# [*store_events*] +# (optional) Save event details. +# Defaults to false +# + +class ceilometer::agent::notification ( + $manage_service = true, + $enabled = true, + $ack_on_event_error = true, + $store_events = false +) { + + include ceilometer::params + + Ceilometer_config<||> ~> Service['ceilometer-agent-notification'] + + Package[$::ceilometer::params::agent_notification_package_name] -> Service['ceilometer-agent-notification'] + ensure_packages([$::ceilometer::params::agent_notification_package_name]) + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + Package['ceilometer-common'] -> Service['ceilometer-agent-notification'] + service { 'ceilometer-agent-notification': + ensure => $service_ensure, + name => $::ceilometer::params::agent_notification_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true + } + + ceilometer_config { + 'notification/ack_on_event_error': value => $ack_on_event_error; + 'notification/store_events' : value => $store_events; + } + +} diff --git a/ceilometer/manifests/alarm/evaluator.pp b/ceilometer/manifests/alarm/evaluator.pp new file mode 100644 index 000000000..2b9ca643b --- /dev/null +++ b/ceilometer/manifests/alarm/evaluator.pp @@ -0,0 +1,71 @@ +# Installs the ceilometer alarm evaluator service +# +# == Params +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true. +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +# [*evaluation_interval*] +# (optional) Define the time interval for the alarm evaluator +# Defaults to 60. +# +# [*evaluation_service*] +# (optional) Define which service use for the evaluator +# Defaults to 'ceilometer.alarm.service.SingletonAlarmService'. +# +# [*partition_rpc_topic*] +# (optional) Define which topic the alarm evaluator should access +# Defaults to 'alarm_partition_coordination'. +# +# [*record_history*] +# (optional) Record alarm change events +# Defaults to true. +# +class ceilometer::alarm::evaluator ( + $manage_service = true, + $enabled = true, + $evaluation_interval = 60, + $evaluation_service = 'ceilometer.alarm.service.SingletonAlarmService', + $partition_rpc_topic = 'alarm_partition_coordination', + $record_history = true, +) { + + include ceilometer::params + + validate_re($evaluation_interval,'^(\d+)$') + + Ceilometer_config<||> ~> Service['ceilometer-alarm-evaluator'] + + Package[$::ceilometer::params::alarm_package_name] -> Service['ceilometer-alarm-evaluator'] + Package[$::ceilometer::params::alarm_package_name] -> Package<| title == 'ceilometer-alarm' |> + ensure_packages($::ceilometer::params::alarm_package_name) + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + Package['ceilometer-common'] -> Service['ceilometer-alarm-evaluator'] + + service { 'ceilometer-alarm-evaluator': + ensure => $service_ensure, + name => $::ceilometer::params::alarm_evaluator_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true + } + + ceilometer_config { + 'alarm/evaluation_interval' : value => $evaluation_interval; + 'alarm/evaluation_service' : value => $evaluation_service; + 'alarm/partition_rpc_topic' : value => $partition_rpc_topic; + 'alarm/record_history' : value => $record_history; + } +} diff --git a/ceilometer/manifests/alarm/notifier.pp b/ceilometer/manifests/alarm/notifier.pp new file mode 100644 index 000000000..4bad22100 --- /dev/null +++ b/ceilometer/manifests/alarm/notifier.pp @@ -0,0 +1,82 @@ +# Installs the ceilometer alarm notifier service +# +# == Params +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true. +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +# [*notifier_rpc_topic*] +# (optional) Define on which topic the notifier will have access. +# Defaults to undef. +# +# [*rest_notifier_certificate_key*] +# (optional) Define the certificate key for the rest service. +# Defaults to undef. +# +# [*rest_notifier_certificate_file*] +# (optional) Define the certificate file for the rest service. +# Defaults to undef. +# +# [*rest_notifier_ssl_verify*] +# (optional) Should the ssl verify parameter be enabled. +# Defaults to true. +# +class ceilometer::alarm::notifier ( + $manage_service = true, + $enabled = true, + $notifier_rpc_topic = undef, + $rest_notifier_certificate_key = undef, + $rest_notifier_certificate_file = undef, + $rest_notifier_ssl_verify = true, +) { + + include ceilometer::params + + validate_bool($rest_notifier_ssl_verify) + + Ceilometer_config<||> ~> Service['ceilometer-alarm-notifier'] + + Package[$::ceilometer::params::alarm_package_name] -> Service['ceilometer-alarm-notifier'] + Package[$::ceilometer::params::alarm_package_name] -> Package<| title == 'ceilometer-alarm' |> + ensure_packages($::ceilometer::params::alarm_package_name) + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + Package['ceilometer-common'] -> Service['ceilometer-alarm-notifier'] + + service { 'ceilometer-alarm-notifier': + ensure => $service_ensure, + name => $::ceilometer::params::alarm_notifier_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true + } + + if $notifier_rpc_topic != undef { + ceilometer_config { + 'alarm/notifier_rpc_topic' : value => $notifier_rpc_topic; + } + } + if $rest_notifier_certificate_key != undef { + ceilometer_config { + 'alarm/rest_notifier_certificate_key' :value => $rest_notifier_certificate_key; + 'alarm/rest_notifier_ssl_verify' :value => $rest_notifier_ssl_verify; + } + } + if $rest_notifier_certificate_file != undef { + ceilometer_config { + 'alarm/rest_notifier_certificate_file' :value => $rest_notifier_certificate_file; + } + } + +} diff --git a/ceilometer/manifests/api.pp b/ceilometer/manifests/api.pp new file mode 100644 index 000000000..70e2bdf4a --- /dev/null +++ b/ceilometer/manifests/api.pp @@ -0,0 +1,130 @@ +# Installs & configure the ceilometer api service +# +# == Parameters +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +# [*keystone_host*] +# (optional) Keystone's admin endpoint IP/Host. +# Defaults to '127.0.0.1' +# +# [*keystone_port*] +# (optional) Keystone's admin endpoint port. +# Defaults to 35357 +# +# [*keystone_auth_admin_prefix*] +# (optional) 'path' to the keystone admin endpoint. +# Define to a path starting with a '/' and without trailing '/'. +# Eg.: '/keystone/admin' to match keystone::wsgi::apache default. +# Defaults to false (empty) +# +# [*keystone_protocol*] +# (optional) 'http' or 'https' +# Defaults to 'https'. +# +# [*keytone_user*] +# (optional) User to authenticate with. +# Defaults to 'ceilometer'. +# +# [*keystone_tenant*] +# (optional) Tenant to authenticate with. +# Defaults to 'services'. +# +# [*keystone_password*] +# Password to authenticate with. +# Mandatory. +# +# [*host*] +# (optional) The ceilometer api bind address. +# Defaults to 0.0.0.0 +# +# [*port*] +# (optional) The ceilometer api port. +# Defaults to 8777 +# + +class ceilometer::api ( + $manage_service = true, + $enabled = true, + $keystone_host = '127.0.0.1', + $keystone_port = '35357', + $keystone_auth_admin_prefix = false, + $keystone_protocol = 'http', + $keystone_user = 'ceilometer', + $keystone_tenant = 'services', + $keystone_password = false, + $keystone_auth_uri = false, + $host = '0.0.0.0', + $port = '8777' +) { + + include ceilometer::params + + validate_string($keystone_password) + + Ceilometer_config<||> ~> Service['ceilometer-api'] + + Package['ceilometer-api'] -> Ceilometer_config<||> + Package['ceilometer-api'] -> Service['ceilometer-api'] + package { 'ceilometer-api': + ensure => installed, + name => $::ceilometer::params::api_package_name, + } + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + Package['ceilometer-common'] -> Service['ceilometer-api'] + service { 'ceilometer-api': + ensure => $service_ensure, + name => $::ceilometer::params::api_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + require => Class['ceilometer::db'], + subscribe => Exec['ceilometer-dbsync'] + } + + ceilometer_config { + 'keystone_authtoken/auth_host' : value => $keystone_host; + 'keystone_authtoken/auth_port' : value => $keystone_port; + 'keystone_authtoken/auth_protocol' : value => $keystone_protocol; + 'keystone_authtoken/admin_tenant_name' : value => $keystone_tenant; + 'keystone_authtoken/admin_user' : value => $keystone_user; + 'keystone_authtoken/admin_password' : value => $keystone_password, secret => true; + 'api/host' : value => $host; + 'api/port' : value => $port; + } + + if $keystone_auth_admin_prefix { + validate_re($keystone_auth_admin_prefix, '^(/.+[^/])?$') + ceilometer_config { + 'keystone_authtoken/auth_admin_prefix': value => $keystone_auth_admin_prefix; + } + } else { + ceilometer_config { + 'keystone_authtoken/auth_admin_prefix': ensure => absent; + } + } + + if $keystone_auth_uri { + ceilometer_config { + 'keystone_authtoken/auth_uri': value => $keystone_auth_uri; + } + } else { + ceilometer_config { + 'keystone_authtoken/auth_uri': value => "${keystone_protocol}://${keystone_host}:5000/"; + } + } + +} diff --git a/ceilometer/manifests/client.pp b/ceilometer/manifests/client.pp new file mode 100644 index 000000000..faa791a18 --- /dev/null +++ b/ceilometer/manifests/client.pp @@ -0,0 +1,20 @@ +# +# Installs the ceilometer python library. +# +# == parameters +# [*ensure*] +# ensure state for pachage. +# +class ceilometer::client ( + $ensure = 'present' +) { + + include ceilometer::params + + package { 'python-ceilometerclient': + ensure => $ensure, + name => $::ceilometer::params::client_package_name, + } + +} + diff --git a/ceilometer/manifests/collector.pp b/ceilometer/manifests/collector.pp new file mode 100644 index 000000000..8e1f6fcb2 --- /dev/null +++ b/ceilometer/manifests/collector.pp @@ -0,0 +1,42 @@ +# Installs the ceilometer collector service +# +# == Params +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true. +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +class ceilometer::collector ( + $manage_service = true, + $enabled = true, +) { + + include ceilometer::params + + Ceilometer_config<||> ~> Service['ceilometer-collector'] + + Package[$::ceilometer::params::collector_package_name] -> Service['ceilometer-collector'] + ensure_packages([$::ceilometer::params::collector_package_name]) + + if $manage_service { + if $enabled { + $service_ensure = 'running' + Class['ceilometer::db'] -> Service['ceilometer-collector'] + Exec['ceilometer-dbsync'] ~> Service['ceilometer-collector'] + } else { + $service_ensure = 'stopped' + } + } + + Package['ceilometer-common'] -> Service['ceilometer-collector'] + service { 'ceilometer-collector': + ensure => $service_ensure, + name => $::ceilometer::params::collector_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true + } +} diff --git a/ceilometer/manifests/config.pp b/ceilometer/manifests/config.pp new file mode 100644 index 000000000..c1d133212 --- /dev/null +++ b/ceilometer/manifests/config.pp @@ -0,0 +1,31 @@ +# == Class: ceilometer::config +# +# This class is used to manage arbitrary ceilometer configurations. +# +# === Parameters +# +# [*ceilometer_config*] +# (optional) Allow configuration of ceilometer.conf. +# +# The value is an hash of ceilometer_config resource. Example: +# { 'DEFAULT/foo' => { value => 'fooValue'}, +# 'DEFAULT/bar' => { value => 'barValue'} +# } +# +# In yaml format, Example: +# ceilometer_config: +# DEFAULT/foo: +# value: fooValue +# DEFAULT/bar: +# value: barValue +# +# NOTE: The configuration MUST NOT be already handled by this module +# or Puppet catalog compilation will fail with duplicate resources. +# +class ceilometer::config ( + $ceilometer_config = {}, +) { + validate_hash($ceilometer_config) + + create_resources('ceilometer_config', $ceilometer_config) +} diff --git a/ceilometer/manifests/db.pp b/ceilometer/manifests/db.pp new file mode 100644 index 000000000..3bed0c138 --- /dev/null +++ b/ceilometer/manifests/db.pp @@ -0,0 +1,80 @@ +# Configures the ceilometer database +# This class will install the required libraries depending on the driver +# specified in the connection_string parameter +# +# == Parameters +# [*database_connection*] +# the connection string. format: [driver]://[user]:[password]@[host]/[database] +# +# [*sync_db*] +# enable dbsync. +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +class ceilometer::db ( + $database_connection = 'mysql://ceilometer:ceilometer@localhost/ceilometer', + $sync_db = true, + $mysql_module = undef, +) { + + include ceilometer::params + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + Package<| title == 'ceilometer-common' |> -> Class['ceilometer::db'] + + validate_re($database_connection, + '(sqlite|mysql|postgresql|mongodb):\/\/(\S+:\S+@\S+\/\S+)?') + + case $database_connection { + /^mysql:\/\//: { + $backend_package = false + + include mysql::bindings::python + } + /^postgres:\/\//: { + $backend_package = $::ceilometer::params::psycopg_package_name + } + /^mongodb:\/\//: { + $backend_package = $::ceilometer::params::pymongo_package_name + } + /^sqlite:\/\//: { + $backend_package = $::ceilometer::params::sqlite_package_name + } + default: { + fail('Unsupported backend configured') + } + } + + if $sync_db { + $command = $::ceilometer::params::dbsync_command + } else { + $command = '/bin/true' + } + + if $backend_package and !defined(Package[$backend_package]) { + package {'ceilometer-backend-package': + ensure => present, + name => $backend_package, + } + } + + ceilometer_config { + 'database/connection': value => $database_connection, secret => true; + } + + Ceilometer_config['database/connection'] ~> Exec['ceilometer-dbsync'] + + exec { 'ceilometer-dbsync': + command => $command, + path => '/usr/bin', + user => $::ceilometer::params::user, + refreshonly => true, + logoutput => on_failure, + subscribe => Ceilometer_config['database/connection'] + } + +} diff --git a/ceilometer/manifests/db/mysql.pp b/ceilometer/manifests/db/mysql.pp new file mode 100644 index 000000000..6fd47c45c --- /dev/null +++ b/ceilometer/manifests/db/mysql.pp @@ -0,0 +1,60 @@ +# The ceilometer::db::mysql class creates a MySQL database for ceilometer. +# It must be used on the MySQL server +# +# == Parameters +# +# [*password*] +# password to connect to the database. Mandatory. +# +# [*dbname*] +# name of the database. Optional. Defaults to ceilometer. +# +# [*user*] +# user to connect to the database. Optional. Defaults to ceilometer. +# +# [*host*] +# the default source host user is allowed to connect from. +# Optional. Defaults to 'localhost' +# +# [*allowed_hosts*] +# other hosts the user is allowd to connect from. +# Optional. Defaults to undef. +# +# [*charset*] +# the database charset. Optional. Defaults to 'utf8' +# +# [*collate*] +# the database collation. Optional. Defaults to 'utf8_unicode_ci' +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +class ceilometer::db::mysql( + $password = false, + $dbname = 'ceilometer', + $user = 'ceilometer', + $host = '127.0.0.1', + $allowed_hosts = undef, + $charset = 'utf8', + $collate = 'utf8_unicode_ci', + $mysql_module = undef, +) { + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + validate_string($password) + + ::openstacklib::db::mysql { 'ceilometer': + user => $user, + password_hash => mysql_password($password), + dbname => $dbname, + host => $host, + charset => $charset, + collate => $collate, + allowed_hosts => $allowed_hosts, + } + + ::Openstacklib::Db::Mysql['ceilometer'] ~> Exec<| title == 'ceilometer-dbsync' |> +} diff --git a/ceilometer/manifests/expirer.pp b/ceilometer/manifests/expirer.pp new file mode 100644 index 000000000..597b09cb4 --- /dev/null +++ b/ceilometer/manifests/expirer.pp @@ -0,0 +1,74 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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: ceilometer::expirer +# +# Setups Ceilometer Expirer service to enable TTL feature. +# +# === Parameters +# +# [*time_to_live*] +# (optional) Number of seconds that samples are kept in the database. +# Should be a valid integer +# Defaults to '-1' to disable TTL and keep forever the datas. +# +# [*minute*] +# (optional) Defaults to '1'. +# +# [*hour*] +# (optional) Defaults to '0'. +# +# [*monthday*] +# (optional) Defaults to '*'. +# +# [*month*] +# (optional) Defaults to '*'. +# +# [*weekday*] +# (optional) Defaults to '*'. +# + +class ceilometer::expirer ( + $time_to_live = '-1', + $minute = 1, + $hour = 0, + $monthday = '*', + $month = '*', + $weekday = '*', +) { + + include ceilometer::params + + Package<| title == 'ceilometer-common' |> -> Class['ceilometer::expirer'] + + ceilometer_config { + 'database/time_to_live': value => $time_to_live; + } + + cron { 'ceilometer-expirer': + command => $ceilometer::params::expirer_command, + environment => 'PATH=/bin:/usr/bin:/usr/sbin', + user => 'ceilometer', + minute => $minute, + hour => $hour, + monthday => $monthday, + month => $month, + weekday => $weekday + } + + +} diff --git a/ceilometer/manifests/init.pp b/ceilometer/manifests/init.pp new file mode 100644 index 000000000..66aada3df --- /dev/null +++ b/ceilometer/manifests/init.pp @@ -0,0 +1,281 @@ +# Class ceilometer +# +# ceilometer base package & configuration +# +# == parameters +# [*metering_secret*] +# secret key for signing messages. Mandatory. +# [*package_ensure*] +# ensure state for package. Optional. Defaults to 'present' +# [*debug*] +# should the daemons log debug messages. Optional. Defaults to 'False' +# [*log_dir*] +# (optional) directory to which ceilometer logs are sent. +# If set to boolean false, it will not log to any directory. +# Defaults to '/var/log/ceilometer' +# [*verbose*] +# should the daemons log verbose messages. Optional. Defaults to 'False' +# [*use_syslog*] +# (optional) Use syslog for logging +# Defaults to false +# [*log_facility*] +# (optional) Syslog facility to receive log lines. +# Defaults to 'LOG_USER' +# [*rpc_backend*] +# (optional) what rpc/queuing service to use +# Defaults to impl_kombu (rabbitmq) +# [*rabbit_host*] +# ip or hostname of the rabbit server. Optional. Defaults to '127.0.0.1' +# [*rabbit_port*] +# port of the rabbit server. Optional. Defaults to 5672. +# [*rabbit_hosts*] +# array of host:port (used with HA queues). Optional. Defaults to undef. +# If defined, will remove rabbit_host & rabbit_port parameters from config +# [*rabbit_userid*] +# user to connect to the rabbit server. Optional. Defaults to 'guest' +# [*rabbit_password*] +# password to connect to the rabbit_server. Optional. Defaults to empty. +# [*rabbit_virtual_host*] +# virtualhost to use. Optional. Defaults to '/' +# [*rabbit_use_ssl*] +# (optional) Connect over SSL for RabbitMQ +# Defaults to false +# [*kombu_ssl_ca_certs*] +# (optional) SSL certification authority file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_certfile*] +# (optional) SSL cert file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_keyfile*] +# (optional) SSL key file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_version*] +# (optional) SSL version to use (valid only if SSL enabled). +# Valid values are TLSv1, SSLv23 and SSLv3. SSLv2 may be +# available on some distributions. +# Defaults to 'SSLv3' +# +# [*qpid_hostname*] +# [*qpid_port*] +# [*qpid_username*] +# [*qpid_password*] +# [*qpid_heartbeat*] +# [*qpid_protocol*] +# [*qpid_tcp_nodelay*] +# [*qpid_reconnect*] +# [*qpid_reconnect_timeout*] +# [*qpid_reconnect_limit*] +# [*qpid_reconnect_interval*] +# [*qpid_reconnect_interval_min*] +# [*qpid_reconnect_interval_max*] +# (optional) various QPID options +# + +class ceilometer( + $metering_secret = false, + $notification_topics = ['notifications'], + $package_ensure = 'present', + $debug = false, + $log_dir = '/var/log/ceilometer', + $verbose = false, + $use_syslog = false, + $log_facility = 'LOG_USER', + $rpc_backend = 'ceilometer.openstack.common.rpc.impl_kombu', + $rabbit_host = '127.0.0.1', + $rabbit_port = 5672, + $rabbit_hosts = undef, + $rabbit_userid = 'guest', + $rabbit_password = '', + $rabbit_virtual_host = '/', + $rabbit_use_ssl = false, + $kombu_ssl_ca_certs = undef, + $kombu_ssl_certfile = undef, + $kombu_ssl_keyfile = undef, + $kombu_ssl_version = 'SSLv3', + $qpid_hostname = 'localhost', + $qpid_port = 5672, + $qpid_username = 'guest', + $qpid_password = 'guest', + $qpid_heartbeat = 60, + $qpid_protocol = 'tcp', + $qpid_tcp_nodelay = true, + $qpid_reconnect = true, + $qpid_reconnect_timeout = 0, + $qpid_reconnect_limit = 0, + $qpid_reconnect_interval_min = 0, + $qpid_reconnect_interval_max = 0, + $qpid_reconnect_interval = 0 +) { + + validate_string($metering_secret) + + include ceilometer::params + + if $kombu_ssl_ca_certs and !$rabbit_use_ssl { + fail('The kombu_ssl_ca_certs parameter requires rabbit_use_ssl to be set to true') + } + if $kombu_ssl_certfile and !$rabbit_use_ssl { + fail('The kombu_ssl_certfile parameter requires rabbit_use_ssl to be set to true') + } + if $kombu_ssl_keyfile and !$rabbit_use_ssl { + fail('The kombu_ssl_keyfile parameter requires rabbit_use_ssl to be set to true') + } + if ($kombu_ssl_certfile and !$kombu_ssl_keyfile) or ($kombu_ssl_keyfile and !$kombu_ssl_certfile) { + fail('The kombu_ssl_certfile and kombu_ssl_keyfile parameters must be used together') + } + + File { + require => Package['ceilometer-common'], + } + + group { 'ceilometer': + name => 'ceilometer', + require => Package['ceilometer-common'], + } + + user { 'ceilometer': + name => 'ceilometer', + gid => 'ceilometer', + system => true, + require => Package['ceilometer-common'], + } + + file { '/etc/ceilometer/': + ensure => directory, + owner => 'ceilometer', + group => 'ceilometer', + mode => '0750', + } + + file { '/etc/ceilometer/ceilometer.conf': + owner => 'ceilometer', + group => 'ceilometer', + mode => '0640', + } + + package { 'ceilometer-common': + ensure => $package_ensure, + name => $::ceilometer::params::common_package_name, + } + + Package['ceilometer-common'] -> Ceilometer_config<||> + + if $rpc_backend == 'ceilometer.openstack.common.rpc.impl_kombu' { + + if $rabbit_hosts { + ceilometer_config { 'DEFAULT/rabbit_host': ensure => absent } + ceilometer_config { 'DEFAULT/rabbit_port': ensure => absent } + ceilometer_config { 'DEFAULT/rabbit_hosts': + value => join($rabbit_hosts, ',') + } + } else { + ceilometer_config { 'DEFAULT/rabbit_host': value => $rabbit_host } + ceilometer_config { 'DEFAULT/rabbit_port': value => $rabbit_port } + ceilometer_config { 'DEFAULT/rabbit_hosts': + value => "${rabbit_host}:${rabbit_port}" + } + } + + if size($rabbit_hosts) > 1 { + ceilometer_config { 'DEFAULT/rabbit_ha_queues': value => true } + } else { + ceilometer_config { 'DEFAULT/rabbit_ha_queues': value => false } + } + + ceilometer_config { + 'DEFAULT/rabbit_userid' : value => $rabbit_userid; + 'DEFAULT/rabbit_password' : value => $rabbit_password, secret => true; + 'DEFAULT/rabbit_virtual_host' : value => $rabbit_virtual_host; + 'DEFAULT/rabbit_use_ssl' : value => $rabbit_use_ssl; + } + + if $rabbit_use_ssl { + + if $kombu_ssl_ca_certs { + ceilometer_config { 'DEFAULT/kombu_ssl_ca_certs': value => $kombu_ssl_ca_certs; } + } else { + ceilometer_config { 'DEFAULT/kombu_ssl_ca_certs': ensure => absent; } + } + + if $kombu_ssl_certfile or $kombu_ssl_keyfile { + ceilometer_config { + 'DEFAULT/kombu_ssl_certfile': value => $kombu_ssl_certfile; + 'DEFAULT/kombu_ssl_keyfile': value => $kombu_ssl_keyfile; + } + } else { + ceilometer_config { + 'DEFAULT/kombu_ssl_certfile': ensure => absent; + 'DEFAULT/kombu_ssl_keyfile': ensure => absent; + } + } + + if $kombu_ssl_version { + ceilometer_config { 'DEFAULT/kombu_ssl_version': value => $kombu_ssl_version; } + } else { + ceilometer_config { 'DEFAULT/kombu_ssl_version': ensure => absent; } + } + + } else { + ceilometer_config { + 'DEFAULT/kombu_ssl_ca_certs': ensure => absent; + 'DEFAULT/kombu_ssl_certfile': ensure => absent; + 'DEFAULT/kombu_ssl_keyfile': ensure => absent; + 'DEFAULT/kombu_ssl_version': ensure => absent; + } + } + + } + + if $rpc_backend == 'ceilometer.openstack.common.rpc.impl_qpid' { + + ceilometer_config { + 'DEFAULT/qpid_hostname' : value => $qpid_hostname; + 'DEFAULT/qpid_port' : value => $qpid_port; + 'DEFAULT/qpid_username' : value => $qpid_username; + 'DEFAULT/qpid_password' : value => $qpid_password, secret => true; + 'DEFAULT/qpid_heartbeat' : value => $qpid_heartbeat; + 'DEFAULT/qpid_protocol' : value => $qpid_protocol; + 'DEFAULT/qpid_tcp_nodelay' : value => $qpid_tcp_nodelay; + 'DEFAULT/qpid_reconnect' : value => $qpid_reconnect; + 'DEFAULT/qpid_reconnect_timeout' : value => $qpid_reconnect_timeout; + 'DEFAULT/qpid_reconnect_limit' : value => $qpid_reconnect_limit; + 'DEFAULT/qpid_reconnect_interval_min': value => $qpid_reconnect_interval_min; + 'DEFAULT/qpid_reconnect_interval_max': value => $qpid_reconnect_interval_max; + 'DEFAULT/qpid_reconnect_interval' : value => $qpid_reconnect_interval; + } + + } + + # Once we got here, we can act as an honey badger on the rpc used. + ceilometer_config { + 'DEFAULT/rpc_backend' : value => $rpc_backend; + 'publisher/metering_secret' : value => $metering_secret, secret => true; + 'DEFAULT/debug' : value => $debug; + 'DEFAULT/verbose' : value => $verbose; + 'DEFAULT/notification_topics' : value => join($notification_topics, ','); + } + + # Log configuration + if $log_dir { + ceilometer_config { + 'DEFAULT/log_dir' : value => $log_dir; + } + } else { + ceilometer_config { + 'DEFAULT/log_dir' : ensure => absent; + } + } + + # Syslog configuration + if $use_syslog { + ceilometer_config { + 'DEFAULT/use_syslog': value => true; + 'DEFAULT/syslog_log_facility': value => $log_facility; + } + } else { + ceilometer_config { + 'DEFAULT/use_syslog': value => false; + } + } + +} diff --git a/ceilometer/manifests/keystone/auth.pp b/ceilometer/manifests/keystone/auth.pp new file mode 100644 index 000000000..b17a7d013 --- /dev/null +++ b/ceilometer/manifests/keystone/auth.pp @@ -0,0 +1,167 @@ +# == Class: ceilometer::keystone::auth +# +# Configures Ceilometer user, service and endpoint in Keystone. +# +# === Parameters +# +# [*password*] +# Password for Ceilometer user. Required. +# +# [*email*] +# Email for Ceilometer user. Optional. Defaults to 'ceilometer@localhost'. +# +# [*auth_name*] +# Username for Ceilometer service. Optional. Defaults to 'ceilometer'. +# +# [*configure_endpoint*] +# Should Ceilometer endpoint be configured? Optional. Defaults to 'true'. +# +# [*configure_user*] +# Should Ceilometer service user be configured? Optional. Defaults to 'true'. +# +# [*configure_user_role*] +# Should roles be configured on Ceilometer service user? Optional. Defaults to 'true'. +# +# [*service_name*] +# Name of the service. Optional. Defaults to value of auth_name. +# +# [*service_type*] +# Type of service. Optional. Defaults to 'metering'. +# +# [*public_address*] +# Public address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*admin_address*] +# Admin address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*internal_address*] +# Internal address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*port*] +# Default port for enpoints. Optional. Defaults to '8777'. +# +# [*region*] +# Region for endpoint. Optional. Defaults to 'RegionOne'. +# +# [*tenant*] +# Tenant for Ceilometer user. Optional. Defaults to 'services'. +# +# [*public_protocol*] +# Protocol for public endpoint. Optional. Defaults to 'http'. +# +# [*admin_protocol*] +# Protocol for admin endpoint. Optional. Defaults to 'http'. +# +# [*internal_protocol*] +# Protocol for public endpoint. Optional. Defaults to 'http'. +# +# [*public_url*] +# The endpoint's public url. +# Optional. Defaults to $public_protocol://$public_address:$port +# This url should *not* contain any API version and should have +# no trailing '/' +# Setting this variable overrides other $public_* parameters. +# +# [*admin_url*] +# The endpoint's admin url. +# Optional. Defaults to $admin_protocol://$admin_address:$port +# This url should *not* contain any API version and should have +# no trailing '/' +# Setting this variable overrides other $admin_* parameters. +# +# [*internal_url*] +# The endpoint's internal url. +# Optional. Defaults to $internal_protocol://$internal_address:$port +# This url should *not* contain any API version and should have +# no trailing '/' +# Setting this variable overrides other $internal_* parameters. +# +class ceilometer::keystone::auth ( + $password = false, + $email = 'ceilometer@localhost', + $auth_name = 'ceilometer', + $configure_user = true, + $configure_user_role = true, + $service_name = undef, + $service_type = 'metering', + $public_address = '127.0.0.1', + $admin_address = '127.0.0.1', + $internal_address = '127.0.0.1', + $port = '8777', + $region = 'RegionOne', + $tenant = 'services', + $public_protocol = 'http', + $admin_protocol = 'http', + $internal_protocol = 'http', + $configure_endpoint = true, + $public_url = undef, + $admin_url = undef, + $internal_url = undef, +) { + + validate_string($password) + + if $public_url { + $public_url_real = $public_url + } else { + $public_url_real = "${public_protocol}://${public_address}:${port}" + } + + if $admin_url { + $admin_url_real = $admin_url + } else { + $admin_url_real = "${admin_protocol}://${admin_address}:${port}" + } + + if $internal_url { + $internal_url_real = $internal_url + } else { + $internal_url_real = "${internal_protocol}://${internal_address}:${port}" + } + + if $service_name { + $real_service_name = $service_name + } else { + $real_service_name = $auth_name + } + + if $configure_user { + keystone_user { $auth_name: + ensure => present, + password => $password, + email => $email, + tenant => $tenant, + } + } + + if $configure_user_role { + Keystone_user_role["${auth_name}@${tenant}"] ~> + Service <| name == 'ceilometer-api' |> + + if !defined(Keystone_role['ResellerAdmin']) { + keystone_role { 'ResellerAdmin': + ensure => present, + } + } + keystone_user_role { "${auth_name}@${tenant}": + ensure => present, + roles => ['admin', 'ResellerAdmin'], + require => Keystone_role['ResellerAdmin'], + } + } + + keystone_service { $real_service_name: + ensure => present, + type => $service_type, + description => 'Openstack Metering Service', + } + if $configure_endpoint { + keystone_endpoint { "${region}/${real_service_name}": + ensure => present, + public_url => $public_url_real, + admin_url => $admin_url_real, + internal_url => $internal_url_real, + } + } +} + diff --git a/ceilometer/manifests/logging.pp b/ceilometer/manifests/logging.pp new file mode 100644 index 000000000..d94d281f0 --- /dev/null +++ b/ceilometer/manifests/logging.pp @@ -0,0 +1,208 @@ +# Class ceilometer::logging +# +# ceilometer extended logging configuration +# +# == parameters +# +# [*logging_context_format_string*] +# (optional) Format string to use for log messages with context. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s\ +# [%(request_id)s %(user_identity)s] %(instance)s%(message)s' +# +# [*logging_default_format_string*] +# (optional) Format string to use for log messages without context. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s\ +# [-] %(instance)s%(message)s' +# +# [*logging_debug_format_suffix*] +# (optional) Formatted data to append to log format when level is DEBUG. +# Defaults to undef. +# Example: '%(funcName)s %(pathname)s:%(lineno)d' +# +# [*logging_exception_prefix*] +# (optional) Prefix each line of exception output with this format. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s' +# +# [*log_config_append*] +# The name of an additional logging configuration file. +# Defaults to undef. +# See https://docs.python.org/2/howto/logging.html +# +# [*default_log_levels*] +# (optional) Hash of logger (keys) and level (values) pairs. +# Defaults to undef. +# Example: +# { 'amqp' => 'WARN', 'amqplib' => 'WARN', 'boto' => 'WARN', +# 'qpid' => 'WARN', 'sqlalchemy' => 'WARN', 'suds' => 'INFO', +# 'iso8601' => 'WARN', +# 'requests.packages.urllib3.connectionpool' => 'WARN' } +# +# [*publish_errors*] +# (optional) Publish error events (boolean value). +# Defaults to undef (false if unconfigured). +# +# [*fatal_deprecations*] +# (optional) Make deprecations fatal (boolean value) +# Defaults to undef (false if unconfigured). +# +# [*instance_format*] +# (optional) If an instance is passed with the log message, format it +# like this (string value). +# Defaults to undef. +# Example: '[instance: %(uuid)s] ' +# +# [*instance_uuid_format*] +# (optional) If an instance UUID is passed with the log message, format +# it like this (string value). +# Defaults to undef. +# Example: instance_uuid_format='[instance: %(uuid)s] ' + +# [*log_date_format*] +# (optional) Format string for %%(asctime)s in log records. +# Defaults to undef. +# Example: 'Y-%m-%d %H:%M:%S' + +class ceilometer::logging( + $logging_context_format_string = undef, + $logging_default_format_string = undef, + $logging_debug_format_suffix = undef, + $logging_exception_prefix = undef, + $log_config_append = undef, + $default_log_levels = undef, + $publish_errors = undef, + $fatal_deprecations = undef, + $instance_format = undef, + $instance_uuid_format = undef, + $log_date_format = undef, +) { + + if $logging_context_format_string { + ceilometer_config { + 'DEFAULT/logging_context_format_string' : + value => $logging_context_format_string; + } + } + else { + ceilometer_config { + 'DEFAULT/logging_context_format_string' : ensure => absent; + } + } + + if $logging_default_format_string { + ceilometer_config { + 'DEFAULT/logging_default_format_string' : + value => $logging_default_format_string; + } + } + else { + ceilometer_config { + 'DEFAULT/logging_default_format_string' : ensure => absent; + } + } + + if $logging_debug_format_suffix { + ceilometer_config { + 'DEFAULT/logging_debug_format_suffix' : + value => $logging_debug_format_suffix; + } + } + else { + ceilometer_config { + 'DEFAULT/logging_debug_format_suffix' : ensure => absent; + } + } + + if $logging_exception_prefix { + ceilometer_config { + 'DEFAULT/logging_exception_prefix' : value => $logging_exception_prefix; + } + } + else { + ceilometer_config { + 'DEFAULT/logging_exception_prefix' : ensure => absent; + } + } + + if $log_config_append { + ceilometer_config { + 'DEFAULT/log_config_append' : value => $log_config_append; + } + } + else { + ceilometer_config { + 'DEFAULT/log_config_append' : ensure => absent; + } + } + + if $default_log_levels { + ceilometer_config { + 'DEFAULT/default_log_levels' : + value => join(sort(join_keys_to_values($default_log_levels, '=')), ','); + } + } + else { + ceilometer_config { + 'DEFAULT/default_log_levels' : ensure => absent; + } + } + + if $publish_errors { + ceilometer_config { + 'DEFAULT/publish_errors' : value => $publish_errors; + } + } + else { + ceilometer_config { + 'DEFAULT/publish_errors' : ensure => absent; + } + } + + if $fatal_deprecations { + ceilometer_config { + 'DEFAULT/fatal_deprecations' : value => $fatal_deprecations; + } + } + else { + ceilometer_config { + 'DEFAULT/fatal_deprecations' : ensure => absent; + } + } + + if $instance_format { + ceilometer_config { + 'DEFAULT/instance_format' : value => $instance_format; + } + } + else { + ceilometer_config { + 'DEFAULT/instance_format' : ensure => absent; + } + } + + if $instance_uuid_format { + ceilometer_config { + 'DEFAULT/instance_uuid_format' : value => $instance_uuid_format; + } + } + else { + ceilometer_config { + 'DEFAULT/instance_uuid_format' : ensure => absent; + } + } + + if $log_date_format { + ceilometer_config { + 'DEFAULT/log_date_format' : value => $log_date_format; + } + } + else { + ceilometer_config { + 'DEFAULT/log_date_format' : ensure => absent; + } + } + + +} diff --git a/ceilometer/manifests/params.pp b/ceilometer/manifests/params.pp new file mode 100644 index 000000000..6ddaf2b54 --- /dev/null +++ b/ceilometer/manifests/params.pp @@ -0,0 +1,79 @@ +# Parameters for puppet-ceilometer +# +class ceilometer::params { + + $dbsync_command = 'ceilometer-dbsync --config-file=/etc/ceilometer/ceilometer.conf' + $expirer_command = 'ceilometer-expirer' + $user = 'ceilometer' + + case $::osfamily { + 'RedHat': { + # package names + $agent_central_package_name = 'openstack-ceilometer-central' + $agent_compute_package_name = 'openstack-ceilometer-compute' + $api_package_name = 'openstack-ceilometer-api' + $collector_package_name = 'openstack-ceilometer-collector' + $agent_notification_package_name = 'openstack-ceilometer-notification' + $alarm_package_name = ['openstack-ceilometer-alarm'] + $common_package_name = 'openstack-ceilometer-common' + $client_package_name = 'python-ceilometerclient' + # service names + $agent_central_service_name = 'openstack-ceilometer-central' + $agent_compute_service_name = 'openstack-ceilometer-compute' + $api_service_name = 'openstack-ceilometer-api' + $collector_service_name = 'openstack-ceilometer-collector' + $alarm_notifier_service_name = 'openstack-ceilometer-alarm-notifier' + $alarm_evaluator_service_name = 'openstack-ceilometer-alarm-evaluator' + $pymongo_package_name = 'python-pymongo' + $psycopg_package_name = 'python-psycopg2' + $agent_notification_service_name = 'openstack-ceilometer-notification' + + # db packages + if $::operatingsystem == 'Fedora' and $::operatingsystemrelease >= 18 { + # fallback to stdlib version, not provided on fedora + $sqlite_package_name = undef + } else { + $sqlite_package_name = 'python-sqlite2' + } + + } + 'Debian': { + # package names + $agent_central_package_name = 'ceilometer-agent-central' + $agent_compute_package_name = 'ceilometer-agent-compute' + $api_package_name = 'ceilometer-api' + $collector_package_name = 'ceilometer-collector' + $agent_notification_package_name = 'ceilometer-agent-notification' + $common_package_name = 'ceilometer-common' + $client_package_name = 'python-ceilometerclient' + $alarm_package_name = ['ceilometer-alarm-notifier','ceilometer-alarm-evaluator'] + # service names + $agent_central_service_name = 'ceilometer-agent-central' + $agent_compute_service_name = 'ceilometer-agent-compute' + $collector_service_name = 'ceilometer-collector' + $api_service_name = 'ceilometer-api' + $agent_notification_service_name = 'ceilometer-agent-notification' + $alarm_notifier_service_name = 'ceilometer-alarm-notifier' + $alarm_evaluator_service_name = 'ceilometer-alarm-evaluator' + # db packages + $pymongo_package_name = 'python-pymongo' + $psycopg_package_name = 'python-psycopg2' + $sqlite_package_name = 'python-pysqlite2' + + # Operating system specific + case $::operatingsystem { + 'Ubuntu': { + $libvirt_group = 'libvirtd' + } + default: { + $libvirt_group = 'libvirt' + } + } + } + default: { + fail("Unsupported osfamily: ${::osfamily} operatingsystem: \ +${::operatingsystem}, module ${module_name} only support osfamily \ +RedHat and Debian") + } + } +} diff --git a/ceilometer/spec/classes/ceilometer_agent_auth_spec.rb b/ceilometer/spec/classes/ceilometer_agent_auth_spec.rb new file mode 100644 index 000000000..4f47eb397 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_agent_auth_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'ceilometer::agent::auth' do + + let :pre_condition do + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + let :params do + { :auth_url => 'http://localhost:5000/v2.0', + :auth_region => 'RegionOne', + :auth_user => 'ceilometer', + :auth_password => 'password', + :auth_tenant_name => 'services', + :enabled => true, + } + end + + shared_examples_for 'ceilometer-agent-auth' do + + it 'configures authentication' do + should contain_ceilometer_config('service_credentials/os_auth_url').with_value('http://localhost:5000/v2.0') + should contain_ceilometer_config('service_credentials/os_region_name').with_value('RegionOne') + should contain_ceilometer_config('service_credentials/os_username').with_value('ceilometer') + should contain_ceilometer_config('service_credentials/os_password').with_value('password') + should contain_ceilometer_config('service_credentials/os_password').with_value(params[:auth_password]).with_secret(true) + should contain_ceilometer_config('service_credentials/os_tenant_name').with_value('services') + should contain_ceilometer_config('service_credentials/os_cacert').with(:ensure => 'absent') + end + + context 'when overriding parameters' do + before do + params.merge!(:auth_cacert => '/tmp/dummy.pem') + end + it { should contain_ceilometer_config('service_credentials/os_cacert').with_value(params[:auth_cacert]) } + end + + end + +end diff --git a/ceilometer/spec/classes/ceilometer_agent_central_spec.rb b/ceilometer/spec/classes/ceilometer_agent_central_spec.rb new file mode 100644 index 000000000..8a45c35e1 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_agent_central_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' + +describe 'ceilometer::agent::central' do + + let :pre_condition do + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + let :params do + { :enabled => true, + :manage_service => true } + end + + shared_examples_for 'ceilometer-agent-central' do + + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer-agent-central package' do + should contain_package('ceilometer-agent-central').with( + :ensure => 'installed', + :name => platform_params[:agent_package_name], + :before => 'Service[ceilometer-agent-central]' + ) + end + + it 'ensures ceilometer-common is installed before the service' do + should contain_package('ceilometer-common').with( + :before => /Service\[ceilometer-agent-central\]/ + ) + end + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures ceilometer-agent-central service' do + should contain_service('ceilometer-agent-central').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:agent_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures ceilometer-agent-central service' do + should contain_service('ceilometer-agent-central').with( + :ensure => nil, + :name => platform_params[:agent_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true + ) + end + end + + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :agent_package_name => 'ceilometer-agent-central', + :agent_service_name => 'ceilometer-agent-central' } + end + + it_configures 'ceilometer-agent-central' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :agent_package_name => 'openstack-ceilometer-central', + :agent_service_name => 'openstack-ceilometer-central' } + end + + it_configures 'ceilometer-agent-central' + end +end diff --git a/ceilometer/spec/classes/ceilometer_agent_compute_spec.rb b/ceilometer/spec/classes/ceilometer_agent_compute_spec.rb new file mode 100644 index 000000000..e68d91e90 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_agent_compute_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +describe 'ceilometer::agent::compute' do + + let :pre_condition do + "include nova\n" + + "include nova::compute\n" + + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + let :params do + { :enabled => true, + :manage_service => true } + end + + shared_examples_for 'ceilometer-agent-compute' do + + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer-agent-compute package' do + should contain_package('ceilometer-agent-compute').with( + :ensure => 'installed', + :name => platform_params[:agent_package_name], + :before => 'Service[ceilometer-agent-compute]' + ) + end + + it 'adds ceilometer user to nova group and, if required, to libvirt group' do + if platform_params[:libvirt_group] + should contain_user('ceilometer').with_groups(['nova', "#{platform_params[:libvirt_group]}"]) + else + should contain_user('ceilometer').with_groups('nova') + end + end + + it 'ensures ceilometer-common is installed before the service' do + should contain_package('ceilometer-common').with( + :before => /Service\[ceilometer-agent-compute\]/ + ) + end + + it 'ensures nova-common is installed before the package ceilometer-common' do + should contain_package('nova-common').with( + :before => /Package\[ceilometer-common\]/ + ) + end + + it 'configures nova notification driver' do + should contain_file_line_after('nova-notification-driver-common').with( + :line => 'notification_driver=nova.openstack.common.notifier.rpc_notifier', + :path => '/etc/nova/nova.conf', + :notify => 'Service[nova-compute]' + ) + should contain_file_line_after('nova-notification-driver-ceilometer').with( + :line => 'notification_driver=ceilometer.compute.nova_notifier', + :path => '/etc/nova/nova.conf', + :notify => 'Service[nova-compute]' + ) + end + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures ceilometer-agent-compute service' do + + should contain_service('ceilometer-agent-compute').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:agent_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures ceilometer-agent-compute service' do + should contain_service('ceilometer-agent-compute').with( + :ensure => nil, + :name => platform_params[:agent_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true + ) + end + end + + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :agent_package_name => 'ceilometer-agent-compute', + :agent_service_name => 'ceilometer-agent-compute' } + end + + context 'on Ubuntu operating systems' do + before do + facts.merge!( :operatingsystem => 'Ubuntu' ) + platform_params.merge!( :libvirt_group => 'libvirtd' ) + end + + it_configures 'ceilometer-agent-compute' + end + + context 'on other operating systems' do + before do + platform_params.merge!( :libvirt_group => 'libvirt' ) + end + + it_configures 'ceilometer-agent-compute' + end + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :agent_package_name => 'openstack-ceilometer-compute', + :agent_service_name => 'openstack-ceilometer-compute' } + end + + it_configures 'ceilometer-agent-compute' + end +end diff --git a/ceilometer/spec/classes/ceilometer_agent_notification_spec.rb b/ceilometer/spec/classes/ceilometer_agent_notification_spec.rb new file mode 100644 index 000000000..8646c28e3 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_agent_notification_spec.rb @@ -0,0 +1,177 @@ +# +# Copyright (C) 2014 eNovance SAS +# +# Author: Emilien Macchi +# +# 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. +# +# Unit tests for ceilometer::agent::notification +# + +require 'spec_helper' + +describe 'ceilometer::agent::notification' do + + let :pre_condition do + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + let :params do + { :manage_service => true, + :enabled => true, + :ack_on_event_error => true, + :store_events => false } + end + + shared_examples_for 'ceilometer-agent-notification' do + + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer agent notification package' do + should contain_package(platform_params[:agent_notification_package_name]) + end + + it 'configures notifications parameters in ceilometer.conf' do + should contain_ceilometer_config('notification/ack_on_event_error').with_value( params[:ack_on_event_error] ) + should contain_ceilometer_config('notification/store_events').with_value( params[:store_events] ) + end + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures ceilometer agent notification service' do + should contain_service('ceilometer-agent-notification').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:agent_notification_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures ceilometer-agent-notification service' do + should contain_service('ceilometer-agent-notification').with( + :ensure => nil, + :name => platform_params[:agent_notification_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true + ) + end + end + + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :agent_notification_package_name => 'ceilometer-agent-notification', + :agent_notification_service_name => 'ceilometer-agent-notification' } + end + + it_configures 'ceilometer-agent-notification' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :agent_notification_package_name => 'openstack-ceilometer-notification', + :agent_notification_service_name => 'openstack-ceilometer-notification' } + end + + it_configures 'ceilometer-agent-notification' + end + + context 'on RHEL 7' do + let :facts do + { :osfamily => 'RedHat', + :operatingsystem => 'RedHat', + :operatingsystemmajrelease => 7 + } + end + + let :platform_params do + { :agent_notification_package_name => 'openstack-ceilometer-notification', + :agent_notification_service_name => 'openstack-ceilometer-notification' } + end + + it_configures 'ceilometer-agent-notification' + end + + context 'on CentOS 7' do + let :facts do + { :osfamily => 'RedHat', + :operatingsystem => 'CentOS', + :operatingsystemmajrelease => 7 + } + end + + let :platform_params do + { :agent_notification_package_name => 'openstack-ceilometer-notification', + :agent_notification_service_name => 'openstack-ceilometer-notification' } + end + + it_configures 'ceilometer-agent-notification' + end + + context 'on Scientific 7' do + let :facts do + { :osfamily => 'RedHat', + :operatingsystem => 'Scientific', + :operatingsystemmajrelease => 7 + } + end + + let :platform_params do + { :agent_notification_package_name => 'openstack-ceilometer-notification', + :agent_notification_service_name => 'openstack-ceilometer-notification' } + end + + it_configures 'ceilometer-agent-notification' + end + + context 'on Fedora 20' do + let :facts do + { :osfamily => 'RedHat', + :operatingsystem => 'Fedora', + :operatingsystemrelease => 20 + } + end + + let :platform_params do + { :agent_notification_package_name => 'openstack-ceilometer-notification', + :agent_notification_service_name => 'openstack-ceilometer-notification' } + end + + it_configures 'ceilometer-agent-notification' + end + +end diff --git a/ceilometer/spec/classes/ceilometer_alarm_evaluator_spec.rb b/ceilometer/spec/classes/ceilometer_alarm_evaluator_spec.rb new file mode 100644 index 000000000..b2c4c48b0 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_alarm_evaluator_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +describe 'ceilometer::alarm::evaluator' do + + let :pre_condition do + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + let :params do + { :evaluation_interval => 60, + :evaluation_service => 'ceilometer.alarm.service.SingletonAlarmService', + :partition_rpc_topic => 'alarm_partition_coordination', + :record_history => true, + :enabled => true, + :manage_service => true, + } + end + + shared_examples_for 'ceilometer-alarm-evaluator' do + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer-alarm package' do + should contain_package(platform_params[:alarm_evaluator_package_name]).with_before('Service[ceilometer-alarm-evaluator]') + should contain_package(platform_params[:alarm_evaluator_package_name]).with( + :ensure => 'present', + :name => platform_params[:alarm_evaluator_package_name] + ) + end + + it 'ensures ceilometer-common is installed before the service' do + should contain_package('ceilometer-common').with( + :before => /Service\[ceilometer-alarm-evaluator\]/ + ) + end + + it 'configures alarm evaluator' do + should contain_ceilometer_config('alarm/evaluation_interval').with_value( params[:evaluation_interval] ) + should contain_ceilometer_config('alarm/evaluation_service').with_value( params[:evaluation_service] ) + should contain_ceilometer_config('alarm/partition_rpc_topic').with_value( params[:partition_rpc_topic] ) + should contain_ceilometer_config('alarm/record_history').with_value( params[:record_history] ) + end + + context 'when overriding parameters' do + before do + params.merge!(:evaluation_interval => 80, + :partition_rpc_topic => 'alarm_partition_coordination', + :record_history => false, + :evaluation_service => 'ceilometer.alarm.service.SingletonTestAlarmService') + end + it { should contain_ceilometer_config('alarm/evaluation_interval').with_value(params[:evaluation_interval]) } + it { should contain_ceilometer_config('alarm/evaluation_service').with_value(params[:evaluation_service]) } + it { should contain_ceilometer_config('alarm/record_history').with_value(params[:record_history]) } + it { should contain_ceilometer_config('alarm/partition_rpc_topic').with_value(params[:partition_rpc_topic]) } + end + + context 'when override the evaluation interval with a non numeric value' do + before do + params.merge!(:evaluation_interval => 'NaN') + end + + it { expect { should contain_ceilometer_config('alarm/evaluation_interval') }.to\ + raise_error(Puppet::Error, /validate_re\(\): .* does not match/) } + end + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures ceilometer-alarm-evaluator service' do + should contain_service('ceilometer-alarm-evaluator').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:alarm_evaluator_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures ceilometer-alarm-evaluator service' do + should contain_service('ceilometer-alarm-evaluator').with( + :ensure => nil, + :name => platform_params[:alarm_evaluator_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true + ) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :alarm_evaluator_package_name => 'ceilometer-alarm-evaluator', + :alarm_evaluator_service_name => 'ceilometer-alarm-evaluator' } + end + + it_configures 'ceilometer-alarm-evaluator' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :alarm_evaluator_package_name => 'openstack-ceilometer-alarm', + :alarm_evaluator_service_name => 'openstack-ceilometer-alarm-evaluator' } + end + + it_configures 'ceilometer-alarm-evaluator' + end + +end diff --git a/ceilometer/spec/classes/ceilometer_alarm_notifier_spec.rb b/ceilometer/spec/classes/ceilometer_alarm_notifier_spec.rb new file mode 100644 index 000000000..d4e65a1e3 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_alarm_notifier_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +describe 'ceilometer::alarm::notifier' do + + let :pre_condition do + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + let :params do + { + #:notifier_rpc_topic => 'UNSET', + #:rest_notifier_certificate_key => 'UNSET', + #:rest_notifier_certificate_file => 'UNSET', + #:rest_notifier_ssl_verify => true, + :enabled => true, + :manage_service => true, + } + end + + shared_examples_for 'ceilometer-alarm-notifier' do + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer-alarm package' do + should contain_package(platform_params[:alarm_notifier_package_name]).with_before('Service[ceilometer-alarm-notifier]') + should contain_package(platform_params[:alarm_notifier_package_name]).with( + :ensure => 'present', + :name => platform_params[:alarm_notifier_package_name] + ) + end + + it 'ensures ceilometer-common is installed before the service' do + should contain_package('ceilometer-common').with( + :before => /Service\[ceilometer-alarm-notifier\]/ + ) + end + + it 'configures alarm notifier' do + should_not contain_ceilometer_config('alarm/notifier_rpc_topic') + should_not contain_ceilometer_config('alarm/rest_notifier_certificate_key') + should_not contain_ceilometer_config('alarm/rest_notifier_certificate_file') + should_not contain_ceilometer_config('alarm/rest_notifier_ssl_verify') + end + + context 'when overriding parameters' do + before do + params.merge!(:notifier_rpc_topic => 'alarm_notifier', + :rest_notifier_certificate_key => '0xdeadbeef', + :rest_notifier_certificate_file => '/var/file', + :rest_notifier_ssl_verify => true) + end + it { should contain_ceilometer_config('alarm/notifier_rpc_topic').with_value(params[:notifier_rpc_topic]) } + it { should contain_ceilometer_config('alarm/rest_notifier_certificate_key').with_value(params[:rest_notifier_certificate_key]) } + it { should contain_ceilometer_config('alarm/rest_notifier_certificate_file').with_value(params[:rest_notifier_certificate_file]) } + it { should contain_ceilometer_config('alarm/rest_notifier_ssl_verify').with_value(params[:rest_notifier_ssl_verify]) } + end + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures ceilometer-alarm-notifier service' do + should contain_service('ceilometer-alarm-notifier').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:alarm_notifier_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures ceilometer-alarm-notifier service' do + should contain_service('ceilometer-alarm-notifier').with( + :ensure => nil, + :name => platform_params[:alarm_notifier_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true + ) + end + end + + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :alarm_notifier_package_name => 'ceilometer-alarm-notifier', + :alarm_notifier_service_name => 'ceilometer-alarm-notifier' } + end + + it_configures 'ceilometer-alarm-notifier' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :alarm_notifier_package_name => 'openstack-ceilometer-alarm', + :alarm_notifier_service_name => 'openstack-ceilometer-alarm-notifier' } + end + + it_configures 'ceilometer-alarm-notifier' + end + +end diff --git a/ceilometer/spec/classes/ceilometer_api_spec.rb b/ceilometer/spec/classes/ceilometer_api_spec.rb new file mode 100644 index 000000000..66a9c1815 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_api_spec.rb @@ -0,0 +1,155 @@ +require 'spec_helper' + +describe 'ceilometer::api' do + + let :pre_condition do + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + let :params do + { :enabled => true, + :manage_service => true, + :keystone_host => '127.0.0.1', + :keystone_port => '35357', + :keystone_protocol => 'http', + :keystone_user => 'ceilometer', + :keystone_password => 'ceilometer-passw0rd', + :keystone_tenant => 'services', + :host => '0.0.0.0', + :port => '8777' + } + end + + shared_examples_for 'ceilometer-api' do + + context 'without required parameter keystone_password' do + before { params.delete(:keystone_password) } + it { expect { should raise_error(Puppet::Error) } } + end + + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer-api package' do + should contain_package('ceilometer-api').with( + :ensure => 'installed', + :name => platform_params[:api_package_name] + ) + end + + it 'configures keystone authentication middleware' do + should contain_ceilometer_config('keystone_authtoken/auth_host').with_value( params[:keystone_host] ) + should contain_ceilometer_config('keystone_authtoken/auth_port').with_value( params[:keystone_port] ) + should contain_ceilometer_config('keystone_authtoken/auth_protocol').with_value( params[:keystone_protocol] ) + should contain_ceilometer_config('keystone_authtoken/admin_tenant_name').with_value( params[:keystone_tenant] ) + should contain_ceilometer_config('keystone_authtoken/admin_user').with_value( params[:keystone_user] ) + should contain_ceilometer_config('keystone_authtoken/admin_password').with_value( params[:keystone_password] ) + should contain_ceilometer_config('keystone_authtoken/admin_password').with_value( params[:keystone_password] ).with_secret(true) + should contain_ceilometer_config('keystone_authtoken/auth_admin_prefix').with_ensure('absent') + should contain_ceilometer_config('keystone_authtoken/auth_uri').with_value( params[:keystone_protocol] + "://" + params[:keystone_host] + ":5000/" ) + should contain_ceilometer_config('api/host').with_value( params[:host] ) + should contain_ceilometer_config('api/port').with_value( params[:port] ) + end + + context 'when specifying keystone_auth_admin_prefix' do + describe 'with a correct value' do + before { params['keystone_auth_admin_prefix'] = '/keystone/admin' } + it { should contain_ceilometer_config('keystone_authtoken/auth_admin_prefix').with_value('/keystone/admin') } + end + + [ + '/keystone/', + 'keystone/', + 'keystone', + '/keystone/admin/', + 'keystone/admin/', + 'keystone/admin' + ].each do |auth_admin_prefix| + describe "with an incorrect value #{auth_admin_prefix}" do + before { params['keystone_auth_admin_prefix'] = auth_admin_prefix } + + it { expect { should contain_ceilomete_config('keystone_authtoken/auth_admin_prefix') }.to \ + raise_error(Puppet::Error, /validate_re\(\): "#{auth_admin_prefix}" does not match/) } + end + end + end + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures ceilometer-api service' do + should contain_service('ceilometer-api').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:api_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true, + :require => 'Class[Ceilometer::Db]', + :subscribe => 'Exec[ceilometer-dbsync]' + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures ceilometer-api service' do + should contain_service('ceilometer-api').with( + :ensure => nil, + :name => platform_params[:api_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true + ) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :api_package_name => 'ceilometer-api', + :api_service_name => 'ceilometer-api' } + end + + it_configures 'ceilometer-api' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :api_package_name => 'openstack-ceilometer-api', + :api_service_name => 'openstack-ceilometer-api' } + end + + it_configures 'ceilometer-api' + end + + describe 'with custom auth_uri' do + let :facts do + { :osfamily => 'RedHat' } + end + before do + params.merge!({ + :keystone_auth_uri => 'https://foo.bar:1234/', + }) + end + it 'should configure custom auth_uri correctly' do + should contain_ceilometer_config('keystone_authtoken/auth_uri').with_value( 'https://foo.bar:1234/' ) + end + end + +end diff --git a/ceilometer/spec/classes/ceilometer_client_spec.rb b/ceilometer/spec/classes/ceilometer_client_spec.rb new file mode 100644 index 000000000..777c931e9 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_client_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'ceilometer::client' do + + shared_examples_for 'ceilometer client' do + + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer client package' do + should contain_package('python-ceilometerclient').with( + :ensure => 'present', + :name => platform_params[:client_package_name] + ) + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :client_package_name => 'python-ceilometerclient' } + end + + it_configures 'ceilometer client' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :client_package_name => 'python-ceilometerclient' } + end + + it_configures 'ceilometer client' + end +end diff --git a/ceilometer/spec/classes/ceilometer_collector_spec.rb b/ceilometer/spec/classes/ceilometer_collector_spec.rb new file mode 100644 index 000000000..72f2510e5 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_collector_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +describe 'ceilometer::collector' do + + let :pre_condition do + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + shared_examples_for 'ceilometer-collector' do + + context 'when enabled' do + before do + pre_condition << "class { 'ceilometer::db': }" + end + + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer-collector package' do + should contain_package(platform_params[:collector_package_name]) + end + + it 'configures ceilometer-collector service' do + should contain_service('ceilometer-collector').with( + :ensure => 'running', + :name => platform_params[:collector_service_name], + :enable => true, + :hasstatus => true, + :hasrestart => true + ) + end + + it 'configures relationships on database' do + should contain_class('ceilometer::db').with_before('Service[ceilometer-collector]') + should contain_exec('ceilometer-dbsync').with_notify('Service[ceilometer-collector]') + end + end + + context 'when disabled' do + let :params do + { :enabled => false } + end + + # Catalog compilation does not crash for lack of ceilometer::db + it { should compile } + + it 'configures ceilometer-collector service' do + should contain_service('ceilometer-collector').with( + :ensure => 'stopped', + :name => platform_params[:collector_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true + ) + end + end + + context 'when service management is disabled' do + let :params do + { :enabled => false, + :manage_service => false } + end + + it 'configures ceilometer-collector service' do + should contain_service('ceilometer-collector').with( + :ensure => nil, + :name => platform_params[:collector_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true + ) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :collector_package_name => 'ceilometer-collector', + :collector_service_name => 'ceilometer-collector' } + end + + it_configures 'ceilometer-collector' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :collector_package_name => 'openstack-ceilometer-collector', + :collector_service_name => 'openstack-ceilometer-collector' } + end + + it_configures 'ceilometer-collector' + end +end diff --git a/ceilometer/spec/classes/ceilometer_config_spec.rb b/ceilometer/spec/classes/ceilometer_config_spec.rb new file mode 100644 index 000000000..33e960356 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_config_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'ceilometer::config' do + + let :params do + { :ceilometer_config => { + 'api/host' => { 'value' => '0.0.0.0'}, + 'api/port' => { 'value' => '8777'}, + }, + } + end + + it 'with [api] options ceilometer_config ' do + should contain_ceilometer_config('api/host').with_value('0.0.0.0') + should contain_ceilometer_config('api/port').with_value('8777') + end + + describe 'with [rpc_notifier2] options ceilometer_config' do + before do + params.merge!({ + :ceilometer_config => { 'rpc_notifier2/topics' => { 'value' => 'notifications'},}, + }) + end + it 'should configure rpc_notifier2 topics correctly' do + should contain_ceilometer_config('rpc_notifier2/topics').with_value('notifications') + end + + end +end diff --git a/ceilometer/spec/classes/ceilometer_db_mysql_spec.rb b/ceilometer/spec/classes/ceilometer_db_mysql_spec.rb new file mode 100644 index 000000000..5d32abad6 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_db_mysql_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe 'ceilometer::db::mysql' do + + let :pre_condition do + 'include mysql::server' + end + + let :params do + { :password => 's3cr3t', + :dbname => 'ceilometer', + :user => 'ceilometer', + :host => 'localhost', + :charset => 'utf8', + :collate => 'utf8_unicode_ci', + } + end + + shared_examples_for 'ceilometer mysql database' do + + context 'when omiting the required parameter password' do + before { params.delete(:password) } + it { expect { should raise_error(Puppet::Error) } } + end + + it 'creates a mysql database' do + should contain_openstacklib__db__mysql( params[:dbname] ).with( + :user => params[:user], + :password_hash => '*58C036CDA51D8E8BBBBF2F9EA5ABF111ADA444F0', + :host => params[:host], + :charset => params[:charset] + ) + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'ceilometer mysql database' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'ceilometer mysql database' + end + + describe "overriding allowed_hosts param to array" do + let :facts do + { :osfamily => "Debian" } + end + let :params do + { + :password => 'ceilometerpass', + :allowed_hosts => ['localhost','%'] + } + end + + end + + describe "overriding allowed_hosts param to string" do + let :facts do + { :osfamily => 'RedHat' } + end + let :params do + { + :password => 'ceilometerpass2', + :allowed_hosts => '192.168.1.1' + } + end + + end + + describe "overriding allowed_hosts param equals to host param " do + let :facts do + { :osfamily => 'RedHat' } + end + let :params do + { + :password => 'ceilometerpass2', + :allowed_hosts => 'localhost' + } + end + + end +end diff --git a/ceilometer/spec/classes/ceilometer_db_spec.rb b/ceilometer/spec/classes/ceilometer_db_spec.rb new file mode 100644 index 000000000..f6526584e --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_db_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +describe 'ceilometer::db' do + + # debian has "python-pymongo" + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :params do + { :database_connection => 'mongodb://localhost:1234/ceilometer', + :sync_db => true } + end + + it { should contain_class('ceilometer::params') } + + it 'installs python-mongodb package' do + should contain_package('ceilometer-backend-package').with( + :ensure => 'present', + :name => 'python-pymongo') + should contain_ceilometer_config('database/connection').with_value('mongodb://localhost:1234/ceilometer') + should contain_ceilometer_config('database/connection').with_value( params[:database_connection] ).with_secret(true) + end + + it 'runs ceilometer-dbsync' do + should contain_exec('ceilometer-dbsync').with( + :command => 'ceilometer-dbsync --config-file=/etc/ceilometer/ceilometer.conf', + :path => '/usr/bin', + :refreshonly => 'true', + :user => 'ceilometer', + :logoutput => 'on_failure' + ) + end + end + + # Fedora > 18 has python-pymongo too + context 'on Redhat platforms' do + let :facts do + { :osfamily => 'Redhat', + :operatingsystem => 'Fedora', + :operatingsystemrelease => 18 + } + end + + let :params do + { :database_connection => 'mongodb://localhost:1234/ceilometer', + :sync_db => false } + end + + it { should contain_class('ceilometer::params') } + + it 'installs pymongo package' do + should contain_package('ceilometer-backend-package').with( + :ensure => 'present', + :name => 'python-pymongo') + should contain_ceilometer_config('database/connection').with_value('mongodb://localhost:1234/ceilometer') + should contain_ceilometer_config('database/connection').with_value( params[:database_connection] ).with_secret(true) + end + + it 'runs ceilometer-dbsync' do + should contain_exec('ceilometer-dbsync').with( + :command => '/bin/true', + :path => '/usr/bin', + :refreshonly => 'true', + :user => 'ceilometer', + :logoutput => 'on_failure' + ) + end + end + + # RHEL has python-pymongo too + context 'on Redhat platforms' do + let :facts do + { :osfamily => 'Redhat', + :operatingsystem => 'CentOS', + :operatingsystemrelease => 6.4 + } + end + + let :params do + { :database_connection => 'mongodb://localhost:1234/ceilometer', + :sync_db => true } + end + + it { should contain_class('ceilometer::params') } + + it 'installs pymongo package' do + should contain_package('ceilometer-backend-package').with( + :ensure => 'present', + :name => 'python-pymongo') + end + + it 'runs ceilometer-dbsync' do + should contain_exec('ceilometer-dbsync').with( + :command => 'ceilometer-dbsync --config-file=/etc/ceilometer/ceilometer.conf', + :path => '/usr/bin', + :refreshonly => 'true', + :user => 'ceilometer', + :logoutput => 'on_failure' + ) + end + end + + # RHEL has python-sqlite2 + context 'on Redhat platforms' do + let :facts do + { :osfamily => 'Redhat', + :operatingsystem => 'CentOS', + :operatingsystemrelease => 6.4 + } + end + + let :params do + { :database_connection => 'sqlite:///var/lib/ceilometer.db', + :sync_db => false } + end + + it { should contain_class('ceilometer::params') } + + it 'installs pymongo package' do + should contain_package('ceilometer-backend-package').with( + :ensure => 'present', + :name => 'python-sqlite2') + should contain_ceilometer_config('database/connection').with_value('sqlite:///var/lib/ceilometer.db') + should contain_ceilometer_config('database/connection').with_value( params[:database_connection] ).with_secret(true) + end + + it 'runs ceilometer-dbsync' do + should contain_exec('ceilometer-dbsync').with( + :command => '/bin/true', + :path => '/usr/bin', + :refreshonly => 'true', + :user => 'ceilometer', + :logoutput => 'on_failure' + ) + end + end + + # debian has "python-pysqlite2" + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :params do + { :database_connection => 'sqlite:///var/lib/ceilometer.db', + :sync_db => true } + end + + it { should contain_class('ceilometer::params') } + + it 'installs python-mongodb package' do + should contain_package('ceilometer-backend-package').with( + :ensure => 'present', + :name => 'python-pysqlite2') + end + + it 'runs ceilometer-dbsync' do + should contain_exec('ceilometer-dbsync').with( + :command => 'ceilometer-dbsync --config-file=/etc/ceilometer/ceilometer.conf', + :path => '/usr/bin', + :refreshonly => 'true', + :user => 'ceilometer', + :logoutput => 'on_failure' + ) + end + end + +end + diff --git a/ceilometer/spec/classes/ceilometer_expirer_spec.rb b/ceilometer/spec/classes/ceilometer_expirer_spec.rb new file mode 100644 index 000000000..08d62fd38 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_expirer_spec.rb @@ -0,0 +1,87 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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. +# +# Unit tests for ceilometer::expirer +# + +require 'spec_helper' + +describe 'ceilometer::expirer' do + + let :pre_condition do + "class { 'ceilometer': metering_secret => 's3cr3t' }" + end + + let :params do + { :time_to_live => '-1' } + end + + shared_examples_for 'ceilometer-expirer' do + + it { should contain_class('ceilometer::params') } + + it 'installs ceilometer common package' do + should contain_package('ceilometer-common').with( + :ensure => 'present', + :name => platform_params[:common_package_name] + ) + end + + it 'configures a cron' do + should contain_cron('ceilometer-expirer').with( + :command => 'ceilometer-expirer', + :environment => 'PATH=/bin:/usr/bin:/usr/sbin', + :user => 'ceilometer', + :minute => 1, + :hour => 0, + :monthday => '*', + :month => '*', + :weekday => '*' + ) + end + + it 'configures database section in ceilometer.conf' do + should contain_ceilometer_config('database/time_to_live').with_value( params[:time_to_live] ) + end + + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :common_package_name => 'ceilometer-common' } + end + + it_configures 'ceilometer-expirer' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :common_package_name => 'openstack-ceilometer-common' } + end + + it_configures 'ceilometer-expirer' + end + +end diff --git a/ceilometer/spec/classes/ceilometer_init_spec.rb b/ceilometer/spec/classes/ceilometer_init_spec.rb new file mode 100644 index 000000000..a4b69188a --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_init_spec.rb @@ -0,0 +1,327 @@ +require 'spec_helper' + +describe 'ceilometer' do + + let :params do + { + :metering_secret => 'metering-s3cr3t', + :package_ensure => 'present', + :debug => 'False', + :log_dir => '/var/log/ceilometer', + :verbose => 'False', + } + end + + let :rabbit_params do + { + :rabbit_host => '127.0.0.1', + :rabbit_port => 5672, + :rabbit_userid => 'guest', + :rabbit_password => '', + :rabbit_virtual_host => '/', + } + end + + let :qpid_params do + { + :rpc_backend => "ceilometer.openstack.common.rpc.impl_qpid", + :qpid_hostname => 'localhost', + :qpid_port => 5672, + :qpid_username => 'guest', + :qpid_password => 'guest', + } + end + + shared_examples_for 'ceilometer' do + + context 'with rabbit_host parameter' do + before { params.merge!( rabbit_params ) } + it_configures 'a ceilometer base installation' + it_configures 'rabbit with SSL support' + it_configures 'rabbit without HA support (with backward compatibility)' + end + + context 'with rabbit_hosts parameter' do + context 'with one server' do + before { params.merge!( rabbit_params ).merge!( :rabbit_hosts => ['127.0.0.1:5672'] ) } + it_configures 'a ceilometer base installation' + it_configures 'rabbit with SSL support' + it_configures 'rabbit without HA support (without backward compatibility)' + end + + context 'with multiple servers' do + before { params.merge!( rabbit_params ).merge!( :rabbit_hosts => ['rabbit1:5672', 'rabbit2:5672'] ) } + it_configures 'a ceilometer base installation' + it_configures 'rabbit with SSL support' + it_configures 'rabbit with HA support' + end + end + + context 'with qpid' do + before {params.merge!( qpid_params ) } + it_configures 'a ceilometer base installation' + it_configures 'qpid support' + end + + end + + shared_examples_for 'a ceilometer base installation' do + + it { should contain_class('ceilometer::params') } + + it 'configures ceilometer group' do + should contain_group('ceilometer').with( + :name => 'ceilometer', + :require => 'Package[ceilometer-common]' + ) + end + + it 'configures ceilometer user' do + should contain_user('ceilometer').with( + :name => 'ceilometer', + :gid => 'ceilometer', + :system => true, + :require => 'Package[ceilometer-common]' + ) + end + + it 'configures ceilometer configuration folder' do + should contain_file('/etc/ceilometer/').with( + :ensure => 'directory', + :owner => 'ceilometer', + :group => 'ceilometer', + :mode => '0750', + :require => 'Package[ceilometer-common]' + ) + end + + it 'configures ceilometer configuration file' do + should contain_file('/etc/ceilometer/ceilometer.conf').with( + :owner => 'ceilometer', + :group => 'ceilometer', + :mode => '0640', + :require => 'Package[ceilometer-common]' + ) + end + + it 'installs ceilometer common package' do + should contain_package('ceilometer-common').with( + :ensure => 'present', + :name => platform_params[:common_package_name] + ) + end + + it 'configures required metering_secret' do + should contain_ceilometer_config('publisher/metering_secret').with_value('metering-s3cr3t') + should contain_ceilometer_config('publisher/metering_secret').with_value( params[:metering_secret] ).with_secret(true) + end + + context 'without the required metering_secret' do + before { params.delete(:metering_secret) } + it { expect { should raise_error(Puppet::Error) } } + end + + it 'configures debug and verbosity' do + should contain_ceilometer_config('DEFAULT/debug').with_value( params[:debug] ) + should contain_ceilometer_config('DEFAULT/verbose').with_value( params[:verbose] ) + end + + it 'configures logging directory by default' do + should contain_ceilometer_config('DEFAULT/log_dir').with_value( params[:log_dir] ) + end + + context 'with logging directory disabled' do + before { params.merge!( :log_dir => false) } + + it { should contain_ceilometer_config('DEFAULT/log_dir').with_ensure('absent') } + end + + it 'configures notification_topics' do + should contain_ceilometer_config('DEFAULT/notification_topics').with_value('notifications') + end + + it 'configures syslog to be disabled by default' do + should contain_ceilometer_config('DEFAULT/use_syslog').with_value('false') + end + + context 'with syslog enabled' do + before { params.merge!( :use_syslog => 'true' ) } + + it { should contain_ceilometer_config('DEFAULT/use_syslog').with_value('true') } + it { should contain_ceilometer_config('DEFAULT/syslog_log_facility').with_value('LOG_USER') } + end + + context 'with syslog enabled and custom settings' do + before { params.merge!( + :use_syslog => 'true', + :log_facility => 'LOG_LOCAL0' + ) } + + it { should contain_ceilometer_config('DEFAULT/use_syslog').with_value('true') } + it { should contain_ceilometer_config('DEFAULT/syslog_log_facility').with_value('LOG_LOCAL0') } + end + + context 'with overriden notification_topics parameter' do + before { params.merge!( :notification_topics => ['notifications', 'custom']) } + + it 'configures notification_topics' do + should contain_ceilometer_config('DEFAULT/notification_topics').with_value('notifications,custom') + end + end + end + + shared_examples_for 'rabbit without HA support (with backward compatibility)' do + + it 'configures rabbit' do + should contain_ceilometer_config('DEFAULT/rabbit_userid').with_value( params[:rabbit_userid] ) + should contain_ceilometer_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ) + should contain_ceilometer_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ).with_secret(true) + should contain_ceilometer_config('DEFAULT/rabbit_virtual_host').with_value( params[:rabbit_virtual_host] ) + end + + it { should contain_ceilometer_config('DEFAULT/rabbit_host').with_value( params[:rabbit_host] ) } + it { should contain_ceilometer_config('DEFAULT/rabbit_port').with_value( params[:rabbit_port] ) } + it { should contain_ceilometer_config('DEFAULT/rabbit_hosts').with_value( "#{params[:rabbit_host]}:#{params[:rabbit_port]}" ) } + it { should contain_ceilometer_config('DEFAULT/rabbit_ha_queues').with_value('false') } + + end + + shared_examples_for 'rabbit without HA support (without backward compatibility)' do + + it 'configures rabbit' do + should contain_ceilometer_config('DEFAULT/rabbit_userid').with_value( params[:rabbit_userid] ) + should contain_ceilometer_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ) + should contain_ceilometer_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ).with_secret(true) + should contain_ceilometer_config('DEFAULT/rabbit_virtual_host').with_value( params[:rabbit_virtual_host] ) + end + + it { should contain_ceilometer_config('DEFAULT/rabbit_host').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/rabbit_port').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/rabbit_hosts').with_value( params[:rabbit_hosts].join(',') ) } + it { should contain_ceilometer_config('DEFAULT/rabbit_ha_queues').with_value('false') } + + end + + shared_examples_for 'rabbit with HA support' do + + it 'configures rabbit' do + should contain_ceilometer_config('DEFAULT/rabbit_userid').with_value( params[:rabbit_userid] ) + should contain_ceilometer_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ) + should contain_ceilometer_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ).with_secret(true) + should contain_ceilometer_config('DEFAULT/rabbit_virtual_host').with_value( params[:rabbit_virtual_host] ) + end + + it { should contain_ceilometer_config('DEFAULT/rabbit_host').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/rabbit_port').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/rabbit_hosts').with_value( params[:rabbit_hosts].join(',') ) } + it { should contain_ceilometer_config('DEFAULT/rabbit_ha_queues').with_value('true') } + + end + + shared_examples_for 'rabbit with SSL support' do + context "with default parameters" do + it { should contain_ceilometer_config('DEFAULT/rabbit_use_ssl').with_value('false') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_version').with_ensure('absent') } + end + + context "with SSL enabled with kombu" do + before { params.merge!( + :rabbit_use_ssl => 'true', + :kombu_ssl_ca_certs => '/path/to/ca.crt', + :kombu_ssl_certfile => '/path/to/cert.crt', + :kombu_ssl_keyfile => '/path/to/cert.key', + :kombu_ssl_version => 'TLSv1' + ) } + + it { should contain_ceilometer_config('DEFAULT/rabbit_use_ssl').with_value('true') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_ca_certs').with_value('/path/to/ca.crt') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_certfile').with_value('/path/to/cert.crt') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_keyfile').with_value('/path/to/cert.key') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_version').with_value('TLSv1') } + end + + context "with SSL enabled without kombu" do + before { params.merge!( + :rabbit_use_ssl => 'true' + ) } + + it { should contain_ceilometer_config('DEFAULT/rabbit_use_ssl').with_value('true') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') } + it { should contain_ceilometer_config('DEFAULT/kombu_ssl_version').with_value('SSLv3') } + end + + context "with SSL wrongly configured" do + context 'with kombu_ssl_ca_certs parameter' do + before { params.merge!(:kombu_ssl_ca_certs => '/path/to/ca.crt') } + it_raises 'a Puppet::Error', /The kombu_ssl_ca_certs parameter requires rabbit_use_ssl to be set to true/ + end + + context 'with kombu_ssl_certfile parameter' do + before { params.merge!(:kombu_ssl_certfile => '/path/to/ssl/cert/file') } + it_raises 'a Puppet::Error', /The kombu_ssl_certfile parameter requires rabbit_use_ssl to be set to true/ + end + + context 'with kombu_ssl_keyfile parameter' do + before { params.merge!(:kombu_ssl_keyfile => '/path/to/ssl/keyfile') } + it_raises 'a Puppet::Error', /The kombu_ssl_keyfile parameter requires rabbit_use_ssl to be set to true/ + end + end + end + + shared_examples_for 'qpid support' do + context("with default parameters") do + it { should contain_ceilometer_config('DEFAULT/qpid_reconnect').with_value(true) } + it { should contain_ceilometer_config('DEFAULT/qpid_reconnect_timeout').with_value('0') } + it { should contain_ceilometer_config('DEFAULT/qpid_reconnect_limit').with_value('0') } + it { should contain_ceilometer_config('DEFAULT/qpid_reconnect_interval_min').with_value('0') } + it { should contain_ceilometer_config('DEFAULT/qpid_reconnect_interval_max').with_value('0') } + it { should contain_ceilometer_config('DEFAULT/qpid_reconnect_interval').with_value('0') } + it { should contain_ceilometer_config('DEFAULT/qpid_heartbeat').with_value('60') } + it { should contain_ceilometer_config('DEFAULT/qpid_protocol').with_value('tcp') } + it { should contain_ceilometer_config('DEFAULT/qpid_tcp_nodelay').with_value(true) } + end + + context("with mandatory parameters set") do + it { should contain_ceilometer_config('DEFAULT/rpc_backend').with_value('ceilometer.openstack.common.rpc.impl_qpid') } + it { should contain_ceilometer_config('DEFAULT/qpid_hostname').with_value( params[:qpid_hostname] ) } + it { should contain_ceilometer_config('DEFAULT/qpid_port').with_value( params[:qpid_port] ) } + it { should contain_ceilometer_config('DEFAULT/qpid_username').with_value( params[:qpid_username]) } + it { should contain_ceilometer_config('DEFAULT/qpid_password').with_value(params[:qpid_password]) } + it { should contain_ceilometer_config('DEFAULT/qpid_password').with_value( params[:qpid_password] ).with_secret(true) } + end + + context("failing if the rpc_backend is not present") do + before { params.delete( :rpc_backend) } + it { expect { should raise_error(Puppet::Error) } } + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :common_package_name => 'ceilometer-common' } + end + + it_configures 'ceilometer' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :common_package_name => 'openstack-ceilometer-common' } + end + + it_configures 'ceilometer' + end +end diff --git a/ceilometer/spec/classes/ceilometer_keystone_auth_spec.rb b/ceilometer/spec/classes/ceilometer_keystone_auth_spec.rb new file mode 100644 index 000000000..1960d34c2 --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_keystone_auth_spec.rb @@ -0,0 +1,218 @@ +require 'spec_helper' + +describe 'ceilometer::keystone::auth' do + + let :default_params do + { + :email => 'ceilometer@localhost', + :auth_name => 'ceilometer', + :configure_endpoint => true, + :service_type => 'metering', + :public_address => '127.0.0.1', + :admin_address => '127.0.0.1', + :internal_address => '127.0.0.1', + :port => '8777', + :region => 'RegionOne', + :tenant => 'services', + :public_protocol => 'http', + :admin_protocol => 'http', + :internal_protocol => 'http' + } + end + + shared_examples_for 'ceilometer keystone auth' do + + context 'without the required password parameter' do + it { expect { should raise_error(Puppet::Error) } } + end + + let :params do + { :password => 'ceil0met3r-passZord' } + end + + context 'with the required parameters' do + it 'configures ceilometer user' do + should contain_keystone_user( default_params[:auth_name] ).with( + :ensure => 'present', + :password => params[:password], + :email => default_params[:email], + :tenant => default_params[:tenant] + ) + end + + it 'configures ceilometer user roles' do + should contain_keystone_user_role("#{default_params[:auth_name]}@#{default_params[:tenant]}").with( + :ensure => 'present', + :roles => ['admin','ResellerAdmin'] + ) + end + + it 'configures ceilometer service' do + should contain_keystone_service( default_params[:auth_name] ).with( + :ensure => 'present', + :type => default_params[:service_type], + :description => 'Openstack Metering Service' + ) + end + + it 'configure ceilometer endpoints' do + should contain_keystone_endpoint("#{default_params[:region]}/#{default_params[:auth_name]}").with( + :ensure => 'present', + :public_url => "#{default_params[:public_protocol]}://#{default_params[:public_address]}:#{default_params[:port]}", + :admin_url => "#{default_params[:admin_protocol]}://#{default_params[:admin_address]}:#{default_params[:port]}", + :internal_url => "#{default_params[:internal_protocol]}://#{default_params[:internal_address]}:#{default_params[:port]}" + ) + end + end + + context 'with overriden parameters' do + before do + params.merge!({ + :email => 'mighty-ceilometer@remotehost', + :auth_name => 'mighty-ceilometer', + :service_type => 'cloud-measuring', + :public_address => '10.0.0.1', + :admin_address => '10.0.0.2', + :internal_address => '10.0.0.3', + :port => '65001', + :region => 'RegionFortyTwo', + :tenant => 'mighty-services', + :public_protocol => 'https', + :admin_protocol => 'ftp', + :internal_protocol => 'gopher' + }) + end + + it 'configures ceilometer user' do + should contain_keystone_user( params[:auth_name] ).with( + :ensure => 'present', + :password => params[:password], + :email => params[:email], + :tenant => params[:tenant] + ) + end + + it 'configures ceilometer user roles' do + should contain_keystone_user_role("#{params[:auth_name]}@#{params[:tenant]}").with( + :ensure => 'present', + :roles => ['admin','ResellerAdmin'] + ) + end + + it 'configures ceilometer service' do + should contain_keystone_service( params[:auth_name] ).with( + :ensure => 'present', + :type => params[:service_type], + :description => 'Openstack Metering Service' + ) + end + + it 'configure ceilometer endpoints' do + should contain_keystone_endpoint("#{params[:region]}/#{params[:auth_name]}").with( + :ensure => 'present', + :public_url => "#{params[:public_protocol]}://#{params[:public_address]}:#{params[:port]}", + :admin_url => "#{params[:admin_protocol]}://#{params[:admin_address]}:#{params[:port]}", + :internal_url => "#{params[:internal_protocol]}://#{params[:internal_address]}:#{params[:port]}" + ) + end + + context 'with overriden full uri' do + before do + params.merge!({ + :public_url => 'https://public.host:443/ceilometer_pub', + :admin_url => 'https://admin.host/ceilometer_adm', + :internal_url => 'http://internal.host:80/ceilometer_int', + }) + end + it 'configure ceilometer endpoints' do + should contain_keystone_endpoint("#{params[:region]}/#{params[:auth_name]}").with( + :ensure => 'present', + :public_url => params[:public_url], + :admin_url => params[:admin_url], + :internal_url => params[:internal_url] + ) + end + end + + context 'with configure_endpoint = false' do + before do + params.delete!(:configure_endpoint) + it 'does not configure ceilometer endpoints' do + should_not contain_keystone_endpoint("#{params[:region]}/#{params[:auth_name]}") + end + end + end + end + + context 'when overriding service name' do + before do + params.merge!({ + :service_name => 'ceilometer_service' + }) + end + it 'configures correct user name' do + should contain_keystone_user('ceilometer') + end + it 'configures correct user role' do + should contain_keystone_user_role('ceilometer@services') + end + it 'configures correct service name' do + should contain_keystone_service('ceilometer_service') + end + it 'configures correct endpoint name' do + should contain_keystone_endpoint('RegionOne/ceilometer_service') + end + end + + context 'when disabling user configuration' do + before do + params.merge!( :configure_user => false ) + end + + it { should_not contain_keystone_user('ceilometer') } + it { should contain_keystone_user_role('ceilometer@services') } + + it { should contain_keystone_service('ceilometer').with( + :ensure => 'present', + :type => 'metering', + :description => 'Openstack Metering Service' + )} + end + + context 'when disabling user and role configuration' do + before do + params.merge!( + :configure_user => false, + :configure_user_role => false + ) + end + + it { should_not contain_keystone_user('ceilometer') } + it { should_not contain_keystone_user_role('ceilometer@services') } + + it { should contain_keystone_service('ceilometer').with( + :ensure => 'present', + :type => 'metering', + :description => 'Openstack Metering Service' + )} + end + + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'ceilometer keystone auth' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'ceilometer keystone auth' + end + +end diff --git a/ceilometer/spec/classes/ceilometer_logging_spec.rb b/ceilometer/spec/classes/ceilometer_logging_spec.rb new file mode 100644 index 000000000..7599ef26a --- /dev/null +++ b/ceilometer/spec/classes/ceilometer_logging_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe 'ceilometer::logging' do + + let :params do + { + } + end + + let :log_params do + { + :logging_context_format_string => '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s', + :logging_default_format_string => '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s', + :logging_debug_format_suffix => '%(funcName)s %(pathname)s:%(lineno)d', + :logging_exception_prefix => '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s', + :log_config_append => '/etc/ceilometer/logging.conf', + :publish_errors => true, + :default_log_levels => { + 'amqp' => 'WARN', 'amqplib' => 'WARN', 'boto' => 'WARN', + 'qpid' => 'WARN', 'sqlalchemy' => 'WARN', 'suds' => 'INFO', + 'iso8601' => 'WARN', + 'requests.packages.urllib3.connectionpool' => 'WARN' }, + :fatal_deprecations => true, + :instance_format => '[instance: %(uuid)s] ', + :instance_uuid_format => '[instance: %(uuid)s] ', + :log_date_format => '%Y-%m-%d %H:%M:%S', + } + end + + shared_examples_for 'ceilometer-logging' do + + context 'with extended logging options' do + before { params.merge!( log_params ) } + it_configures 'logging params set' + end + + context 'without extended logging options' do + it_configures 'logging params unset' + end + + end + + shared_examples_for 'logging params set' do + it 'enables logging params' do + should contain_ceilometer_config('DEFAULT/logging_context_format_string').with_value( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s') + + should contain_ceilometer_config('DEFAULT/logging_default_format_string').with_value( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s') + + should contain_ceilometer_config('DEFAULT/logging_debug_format_suffix').with_value( + '%(funcName)s %(pathname)s:%(lineno)d') + + should contain_ceilometer_config('DEFAULT/logging_exception_prefix').with_value( + '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s') + + should contain_ceilometer_config('DEFAULT/log_config_append').with_value( + '/etc/ceilometer/logging.conf') + should contain_ceilometer_config('DEFAULT/publish_errors').with_value( + true) + + should contain_ceilometer_config('DEFAULT/default_log_levels').with_value( + 'amqp=WARN,amqplib=WARN,boto=WARN,iso8601=WARN,qpid=WARN,requests.packages.urllib3.connectionpool=WARN,sqlalchemy=WARN,suds=INFO') + + should contain_ceilometer_config('DEFAULT/fatal_deprecations').with_value( + true) + + should contain_ceilometer_config('DEFAULT/instance_format').with_value( + '[instance: %(uuid)s] ') + + should contain_ceilometer_config('DEFAULT/instance_uuid_format').with_value( + '[instance: %(uuid)s] ') + + should contain_ceilometer_config('DEFAULT/log_date_format').with_value( + '%Y-%m-%d %H:%M:%S') + end + end + + + shared_examples_for 'logging params unset' do + [ :logging_context_format_string, :logging_default_format_string, + :logging_debug_format_suffix, :logging_exception_prefix, + :log_config_append, :publish_errors, + :default_log_levels, :fatal_deprecations, + :instance_format, :instance_uuid_format, + :log_date_format, ].each { |param| + it { should contain_ceilometer_config("DEFAULT/#{param}").with_ensure('absent') } + } + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'ceilometer-logging' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'ceilometer-logging' + end + +end diff --git a/ceilometer/spec/shared_examples.rb b/ceilometer/spec/shared_examples.rb new file mode 100644 index 000000000..d92156a36 --- /dev/null +++ b/ceilometer/spec/shared_examples.rb @@ -0,0 +1,5 @@ +shared_examples_for "a Puppet::Error" do |description| + it "with message matching #{description.inspect}" do + expect { should have_class_count(1) }.to raise_error(Puppet::Error, description) + end +end diff --git a/ceilometer/spec/spec_helper.rb b/ceilometer/spec/spec_helper.rb new file mode 100644 index 000000000..53d4dd02d --- /dev/null +++ b/ceilometer/spec/spec_helper.rb @@ -0,0 +1,7 @@ +require 'puppetlabs_spec_helper/module_spec_helper' +require 'shared_examples' + +RSpec.configure do |c| + c.alias_it_should_behave_like_to :it_configures, 'configures' + c.alias_it_should_behave_like_to :it_raises, 'raises' +end diff --git a/certmonger b/certmonger deleted file mode 160000 index 5fbf10fbb..000000000 --- a/certmonger +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5fbf10fbbff4aed4db30e839c63c99b195e8425a diff --git a/certmonger/LICENSE b/certmonger/LICENSE new file mode 100644 index 000000000..3fa0da30c --- /dev/null +++ b/certmonger/LICENSE @@ -0,0 +1,13 @@ +Copyright 2013 Red Hat, Inc. + +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/certmonger/Modulefile b/certmonger/Modulefile new file mode 100644 index 000000000..2bc1a0099 --- /dev/null +++ b/certmonger/Modulefile @@ -0,0 +1,10 @@ +name 'rcritten/certmonger' +version '1.0.2' +source 'git://github.com/rcritten/puppet-certmonger.git' +author 'Rob Crittenden ' +license 'Apache' +summary 'Certmonger Puppet Module' +description 'This module tracks certificates using certmonger.' +project_page 'https://github.com/rcritten/puppet-certmonger' + +dependency 'puppetlabs/stdlib', '>= 0.0.1' diff --git a/certmonger/README.md b/certmonger/README.md new file mode 100644 index 000000000..edcb46d38 --- /dev/null +++ b/certmonger/README.md @@ -0,0 +1,34 @@ +# certmonger puppet module + +very simple puppet module to request IPA certs via certmonger. + +This requires that the machine already be enrolled in an IPA server + +When using an NSS database this has a side-effect of creating a file +in the enrolled subdirectory of the NSS database named after the principal. +This is an indicator that the certificate has already been requested. + +Be aware of SELinux too. You can't just put NSS databases in any directory +you want. The certmonger status will probably be in NEED_KEY_PAIR in the +case of a AVC. + +certmonger uses the host principal on the machine to communicate with IPA. +By default, host principals do not have permission to add new services. +This means you'll probably need to pre-create the services, otherwise +you'll get a status like this: + +Request ID '20130823131914': + status: CA_REJECTED + ca-error: Server denied our request, giving up: 2100 (RPC failed at server. Insufficient access: You need to be a member of the serviceadmin role to add services). + +When generating an OpenSSL certificate we need to let certmonger create the +key and cert files. If puppet does it first then certmonger will think that +the user provided a key to use (a blank file) and things fail. The current +workaround is to call getcert -f /path/to/cert to make sure the key exists. + +TESTING + +Between OpenSSL invocations you'll want to clean up. This is what I do: + +getcert stop-tracking -i `getcert list -f /tmp/test.crt |grep ID |cut -d\' -f2` +rm -f /tmp/test.key /tmp/test.crt diff --git a/certmonger/lib/facter/ipa_client_configured.rb b/certmonger/lib/facter/ipa_client_configured.rb new file mode 100644 index 000000000..cc50290c2 --- /dev/null +++ b/certmonger/lib/facter/ipa_client_configured.rb @@ -0,0 +1,9 @@ +Facter.add("ipa_client_configured") do + setcode do + if File.exist? "/etc/ipa/default.conf" + "true" + else + "false" + end + end +end diff --git a/certmonger/manifests/request_ipa_cert.pp b/certmonger/manifests/request_ipa_cert.pp new file mode 100644 index 000000000..d3747ce07 --- /dev/null +++ b/certmonger/manifests/request_ipa_cert.pp @@ -0,0 +1,164 @@ +# Request a new certificate from IPA using certmonger +# +# Parameters: +# $dbname - required for nss, unused for openssl. The directory +# to store the db +# $seclib, - required - select nss or openssl +# $principal - required - the IPA principal to associate the +# certificate with +# $nickname - required for nss, unused for openssl. The NSS +# certificate nickname +# $cert - required for openssl, unused for nss. The full file +# path to store the certificate in. +# $key - required for openssl, unused for nss. The full file +# path to store the key in +# $basedir - The base directory for $dbname, defaults +# to '/etc/pki'. Not used with openssl. +# $owner_id - owner of OpenSSL cert and key files +# $group_id - group of OpenSSL cert and key files +# $hostname - hostname in the subject of the certificate. +# defaults to current fqdn. +# +# Actions: +# Submits a certificate request to an IPA server for a new certificate. +# +# Requires: +# The NSS db must already exist. It can be created using the nssdb +# module. +# +# Sample Usage: +# +# NSS: +# certmonger::request_ipa_cert {'test': +# seclib => "nss", +# nickname => "broker", +# principal => "qpid/${fqdn}"} +# +# OpenSSL: +# certmonger::request_ipa_cert {'test': +# seclib => "openssl", +# principal => "qpid/${fqdn}", +# key => "/etc/pki/test2/test2.key", +# cert => "/etc/pki/test2/test2.crt", +# owner_id => 'qpidd', +# group_id => 'qpidd'} + +define certmonger::request_ipa_cert ( + $dbname = $title, + $seclib, + $principal, + $nickname = undef, + $cert = undef, + $key = undef, + $basedir = '/etc/pki', + $owner_id = undef, + $group_id = undef, + $hostname = undef +) { + include certmonger::server + + if "$ipa_client_configured" == 'true' { + + $principal_no_slash = regsubst($principal, '\/', '_') + + if $hostname == undef { + $subject = '' + } else { + $subject = "-N cn=${hostname}" + } + + if $seclib == 'nss' { + $options = "-d ${basedir}/${dbname} -n ${nickname} -p ${basedir}/${dbname}/password.conf" + + file {"${basedir}/${dbname}/requested": + ensure => directory, + mode => 0600, + owner => 0, + group => 0, + } + + # Semaphore file to determine if we've already requested a certificate. + file {"${basedir}/${dbname}/requested/${principal_no_slash}": + ensure => file, + mode => 0600, + owner => $owner_id, + group => $group_id, + require => [ + Exec["get_cert_nss_${title}"] + ], + } + exec {"get_cert_nss_${title}": + command => "/usr/bin/ipa-getcert request ${options} -K ${principal} ${subject}", + creates => "${basedir}/${dbname}/requested/${principal_no_slash}", + require => [ + Package['certmonger'], + File["${basedir}/${dbname}/password.conf"], + ], + } + } + elsif $seclib == 'openssl' { + + $options = "-k ${key} -f ${cert}" + + # NOTE: Order is extremely important here. If the key file exists + # (content doesn't matter) then certmonger will attempt to use that + # as the key. You could end up in a NEWLY_ADDED_NEED_KEYINFO_READ_PIN + # state if the key file doesn't actually contain a key. + + file {"${cert}": + ensure => file, + mode => 0444, + owner => $owner_id, + group => $group_id, + } + file {"${key}": + ensure => file, + mode => 0440, + owner => $owner_id, + group => $group_id, + } + exec {"get_cert_openssl_${title}": + command => "/usr/bin/ipa-getcert request ${options} -K ${principal} ${subject}", + creates => [ + "${key}", + "${cert}", + ], + require => [ + Package['certmonger'], + ], + before => [ + File["${key}"], + File["${cert}"], + ], + notify => Exec["wait_for_certmonger_${title}"], + } + + # We need certmonger to finish creating the key before we + # can proceed. Use onlyif as a way to execute multiple + # commands without restorting to shipping a shell script. + # This will call getcert to check the status of our cert + # 5 times. This doesn't short circuit though, so all 5 will + # always run, causing a 5-second delay. + exec {"wait_for_certmonger_${title}": + command => "true", + onlyif => [ + "sleep 1 && getcert list -f ${cert}", + "sleep 1 && getcert list -f ${cert}", + "sleep 1 && getcert list -f ${cert}", + "sleep 1 && getcert list -f ${cert}", + "sleep 1 && getcert list -f ${cert}", + ], + path => "/usr/bin:/bin", + before => [ + File["${key}"], + File["${cert}"], + ], + refreshonly => true, + } + } else { + fail("Unrecognized security library: ${seclib}") + } + } else { + fail("ipa not configured") + } +} diff --git a/certmonger/manifests/server.pp b/certmonger/manifests/server.pp new file mode 100644 index 000000000..0755f1b5e --- /dev/null +++ b/certmonger/manifests/server.pp @@ -0,0 +1,11 @@ +class certmonger::server { + + package { 'certmonger': ensure => present } + + service { 'certmonger': + name => 'certmonger', + ensure => running, + enable => true, + require => Package['certmonger'], + } +} diff --git a/certmonger/tests/nss_ipa.pp b/certmonger/tests/nss_ipa.pp new file mode 100644 index 000000000..1b8de5f93 --- /dev/null +++ b/certmonger/tests/nss_ipa.pp @@ -0,0 +1,6 @@ +certmonger::request_ipa_cert {'test': + seclib => "nss", + nickname => "Server-Cert", + principal => "nss_test/${fqdn}", + basedir => '/tmp/nssdb', +} diff --git a/certmonger/tests/openssl_ipa.pp b/certmonger/tests/openssl_ipa.pp new file mode 100644 index 000000000..80e843d21 --- /dev/null +++ b/certmonger/tests/openssl_ipa.pp @@ -0,0 +1,8 @@ +certmonger::request_ipa_cert {'test': + seclib => "openssl", + principal => "openssl_test/${fqdn}", + key => "/tmp/test.key", + cert => "/tmp/test.crt", + owner_id => 'rcrit', + group_id => 'rcrit' +} diff --git a/cinder b/cinder deleted file mode 160000 index 2da616a4a..000000000 --- a/cinder +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2da616a4a52d3086fe3a291b9199fc7313575504 diff --git a/cinder/.fixtures.yml b/cinder/.fixtures.yml new file mode 100644 index 000000000..563283906 --- /dev/null +++ b/cinder/.fixtures.yml @@ -0,0 +1,20 @@ +fixtures: + repositories: + 'apt': 'git://github.com/puppetlabs/puppetlabs-apt.git' + 'inifile': 'git://github.com/puppetlabs/puppetlabs-inifile' + 'keystone': 'git://github.com/stackforge/puppet-keystone.git' + 'mysql': + repo: 'git://github.com/puppetlabs/puppetlabs-mysql.git' + ref: 'origin/2.2.x' + 'openstacklib': 'git://github.com/stackforge/puppet-openstacklib.git' + 'postgresql': + repo: 'git://github.com/puppetlabs/puppet-postgresql.git' + ref: '2.5.0' + 'qpid': 'git://github.com/dprince/puppet-qpid.git' + 'rabbitmq': + repo: 'git://github.com/puppetlabs/puppetlabs-rabbitmq' + ref: 'origin/2.x' + 'stdlib': 'git://github.com/puppetlabs/puppetlabs-stdlib.git' + 'sysctl': 'git://github.com/duritong/puppet-sysctl.git' + symlinks: + 'cinder': "#{source_dir}" diff --git a/cinder/.gitignore b/cinder/.gitignore new file mode 100644 index 000000000..1fc755c8f --- /dev/null +++ b/cinder/.gitignore @@ -0,0 +1,5 @@ +Gemfile.lock +spec/fixtures/modules/* +spec/fixtures/manifests/site.pp +*.swp +pkg diff --git a/cinder/.gitreview b/cinder/.gitreview new file mode 100644 index 000000000..8afd6562d --- /dev/null +++ b/cinder/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=stackforge/puppet-cinder.git diff --git a/cinder/Gemfile b/cinder/Gemfile new file mode 100644 index 000000000..d965fa900 --- /dev/null +++ b/cinder/Gemfile @@ -0,0 +1,18 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'puppetlabs_spec_helper', :require => false + gem 'puppet-lint', '~> 0.3.2' + gem 'rake', '10.1.1' + gem 'rspec', '< 2.99' + gem 'json' + gem 'webmock' +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/cinder/LICENSE b/cinder/LICENSE new file mode 100644 index 000000000..8d968b6cb --- /dev/null +++ b/cinder/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/cinder/Modulefile b/cinder/Modulefile new file mode 100644 index 000000000..d68b5159a --- /dev/null +++ b/cinder/Modulefile @@ -0,0 +1,15 @@ +name 'puppetlabs-cinder' +version '4.0.0' +author 'Puppet Labs and StackForge Contributors' +license 'Apache License 2.0' +summary 'Puppet module for OpenStack Cinder' +description 'Installs and configures OpenStack Cinder (Block Storage).' +project_page 'https://launchpad.net/puppet-cinder' +source 'https://github.com/stackforge/puppet-cinder' + +dependency 'dprince/qpid', '>=1.0.0 <2.0.0' +dependency 'puppetlabs/inifile', '>=1.0.0 <2.0.0' +dependency 'puppetlabs/keystone', '>=4.0.0 <5.0.0' +dependency 'puppetlabs/rabbitmq', '>=2.0.2 <4.0.0' +dependency 'puppetlabs/stdlib', '>=4.0.0' +dependency 'stackforge/openstacklib', '>=5.0.0' diff --git a/cinder/README.md b/cinder/README.md new file mode 100644 index 000000000..05d67681d --- /dev/null +++ b/cinder/README.md @@ -0,0 +1,248 @@ +cinder +======= + +4.0.0 - 2014.1.0 - Icehouse + +#### Table of Contents + +1. [Overview - What is the cinder module?](#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with cinder](#setup) +4. [Implementation - An under-the-hood peek at what the module is doing](#implementation) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) +7. [Contributors - Those with commits](#contributors) +8. [Release Notes - Notes on the most recent updates to the module](#release-notes) + +Overview +-------- + +The cinder module is a part of [Stackforge](https://github.com/stackfoge), an effort by the Openstack infrastructure team to provide continuous integration testing and code review for Openstack and Openstack community projects not part of the core software. The module its self is used to flexibly configure and manage the block storage service for Openstack. + +Module Description +------------------ + +The cinder module is a thorough attempt to make Puppet capable of managing the entirety of cinder. This includes manifests to provision such things as keystone endpoints, RPC configurations specific to cinder, and database connections. Types are shipped as part of the cinder module to assist in manipulation of configuration files. + +This module is tested in combination with other modules needed to build and leverage an entire Openstack software stack. These modules can be found, all pulled together in the [openstack module](https://github.com/stackfoge/puppet-openstack). + +Setup +----- + +**What the cinder module affects** + +* cinder, the block storage service for Openstack. + +### Installing cinder + + puppet module install puppetlabs/cinder + +### Beginning with cinder + +To utilize the cinder module's functionality you will need to declare multiple resources. The following is a modified excerpt from the [openstack module](https://github.com/stackfoge/puppet-openstack). This is not an exhaustive list of all the components needed, we recommend you consult and understand the [openstack module](https://github.com/stackforge/puppet-openstack) and the [core openstack](http://docs.openstack.org) documentation. + +**Define a cinder control node** + +```puppet +class { 'cinder': + database_connection => 'mysql://cinder:secret_block_password@openstack-controller.example.com/cinder', + rabbit_password => 'secret_rpc_password_for_blocks', + rabbit_host => 'openstack-controller.example.com', + verbose => true, +} + +class { 'cinder::api': + keystone_password => $keystone_password, + keystone_enabled => $keystone_enabled, + keystone_user => $keystone_user, + keystone_auth_host => $keystone_auth_host, + keystone_auth_port => $keystone_auth_port, + keystone_auth_protocol => $keystone_auth_protocol, + service_port => $keystone_service_port, + package_ensure => $cinder_api_package_ensure, + bind_host => $cinder_bind_host, + enabled => $cinder_api_enabled, +} + +class { 'cinder::scheduler': + scheduler_driver => 'cinder.scheduler.simple.SimpleScheduler', +} +``` + +**Define a cinder storage node** + +```puppet +class { 'cinder': + database_connection => 'mysql://cinder:secret_block_password@openstack-controller.example.com/cinder', + rabbit_password => 'secret_rpc_password_for_blocks', + rabbit_host => 'openstack-controller.example.com', + verbose => true, +} + +class { 'cinder::volume': } + +class { 'cinder::volume::iscsi': + iscsi_ip_address => '10.0.0.2', +} +``` + +**Define a cinder storage node with multiple backends ** + +```puppet +class { 'cinder': + database_connection => 'mysql://cinder:secret_block_password@openstack-controller.example.com/cinder', + rabbit_password => 'secret_rpc_password_for_blocks', + rabbit_host => 'openstack-controller.example.com', + verbose => true, +} + +class { 'cinder::volume': } + +cinder::backend::iscsi {'iscsi1': + iscsi_ip_address => '10.0.0.2', +} + +cinder::backend::iscsi {'iscsi2': + iscsi_ip_address => '10.0.0.3', +} + +cinder::backend::iscsi {'iscsi3': + iscsi_ip_address => '10.0.0.4', + volume_backend_name => 'iscsi', +} + +cinder::backend::iscsi {'iscsi4': + iscsi_ip_address => '10.0.0.5', + volume_backend_name => 'iscsi', +} + +cinder::backend::rbd {'rbd-images': + rbd_pool => 'images', + rbd_user => 'images', +} + +# Cinder::Type requires keystone credentials +Cinder::Type { + os_password => 'admin', + os_tenant_name => 'admin', + os_username => 'admin', + os_auth_url => 'http://127.0.0.1:5000/v2.0/', +} + +cinder::type {'iscsi': + set_key => 'volume_backend_name', + set_value => ['iscsi1', 'iscsi2', 'iscsi'] +} + +cinder::type {'rbd': + set_key => 'volume_backend_name', + set_value => 'rbd-images', +} + +class { 'cinder::backends': + enabled_backends => ['iscsi1', 'iscsi2', 'rbd-images'] +} +``` + +Note: that the name passed to any backend resource must be unique accross all backends otherwise a duplicate resource will be defined. + +** Using type and type_set ** + +Cinder allows for the usage of type to set extended information that can be used for various reasons. We have resource provider for ``type`` and ``type_set`` Since types are rarely defined with out also setting attributes with it, the resource for ``type`` can also call ``type_set`` if you pass ``set_key`` and ``set_value`` + + +Implementation +-------------- + +### cinder + +cinder is a combination of Puppet manifest and ruby code to delivery configuration and extra functionality through types and providers. + +Limitations +------------ + +* Setup of storage nodes is limited to Linux and LVM, i.e. Puppet won't configure a Nexenta appliance but nova can be configured to use the Nexenta driver with Class['cinder::volume::nexenta']. + +Development +----------- + +Developer documentation for the entire puppet-openstack project. + +* https://wiki.openstack.org/wiki/Puppet-openstack#Developer_documentation + +Contributors +------------ + +* https://github.com/stackforge/puppet-cinder/graphs/contributors + +Release Notes +------------- + +**4.0.0** + +* Stable Icehouse release. +* Updated NetApp unified driver config options. +* Updated support for latest RabbitMQ module. +* Added Glance support. +* Added GlusterFS driver support. +* Added region support. +* Added support for MySQL module (>= 2.2). +* Added support for Swift and Ceph backup backend. +* Added cinder::config to handle additional custom options. +* Refactored duplicate code for single and multiple backends. +* Removed control exchange flag. +* Removed deprecated cinder::base class. + +**3.1.1** + +* Fixed resource duplication bug. + +**3.1.0** + +* Added default_volume_type as a Cinder API parameter. +* Added parameter for endpoint procols. +* Deprecated glance_api_version. +* Added support for VMDK. +* Added support for Cinder multi backend. +* Added support for https authentication endpoints. +* Replaced pip with native package manager (VMDK). + +**3.0.0** + +* Major release for OpenStack Havana. +* Added support for SolidFire. +* Added support for ceilometer. +* Fixed bug for cinder-volume requirement. + +**2.2.0** + +* Added support for rate limiting via api-paste.ini +* Added support to configure control_exchange. +* Added parameter check to enable or disable db_sync. +* Added syslog support. +* Added default auth_uri setting for auth token. +* Set package defaults to present. +* Fixed a bug to create empty init script when necessary. +* Various lint fixes. + +**2.1.0** + +* Added configuration of Cinder quotas. +* Added support for NetApp direct driver backend. +* Added support for ceph backend. +* Added support for SQL idle timeout. +* Added support for RabbitMQ clustering with single IP. +* Fixed allowed_hosts/database connection bug. +* Fixed lvm2 setup failure for Ubuntu. +* Removed unnecessary mysql::server dependency. +* Pinned RabbitMQ and database module versions. +* Various lint and bug fixes. + +**2.0.0** + +* Upstream is now part of stackfoge. +* Nexenta, NFS, and SAN support added as cinder volume drivers. +* Postgres support added. +* The Apache Qpid and the RabbitMQ message brokers available as RPC backends. +* Configurability of scheduler_driver. +* Various cleanups and bug fixes. diff --git a/cinder/Rakefile b/cinder/Rakefile new file mode 100644 index 000000000..4c2b2ed07 --- /dev/null +++ b/cinder/Rakefile @@ -0,0 +1,6 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings = true +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_parameter_defaults') diff --git a/cinder/examples/cinder_volume_with_pacemaker.pp b/cinder/examples/cinder_volume_with_pacemaker.pp new file mode 100644 index 000000000..4a38db7f7 --- /dev/null +++ b/cinder/examples/cinder_volume_with_pacemaker.pp @@ -0,0 +1,40 @@ +# Example: managing cinder controller services with pacemaker +# +# By setting enabled to false, these services will not be started at boot. By setting +# manage_service to false, puppet will not kill these services on every run. This +# allows the Pacemaker resource manager to dynamically determine on which node each +# service should run. +# +# The puppet commands below would ideally be applied to at least three nodes. +# +# Note that cinder-api is associated with the virtual IP address as +# it is called from external services. The remaining services connect to the +# database and/or message broker independently. +# +# Example pacemaker resource configuration commands (configured once per cluster): +# +# sudo pcs resource create cinder_vip ocf:heartbeat:IPaddr2 params ip=192.0.2.3 \ +# cidr_netmask=24 op monitor interval=10s +# +# sudo pcs resource create cinder_api_service lsb:openstack-cinder-api +# sudo pcs resource create cinder_scheduler_service lsb:openstack-cinder-scheduler +# +# sudo pcs constraint colocation add cinder_api_service with cinder_vip + +class { 'cinder': + database_connection => 'mysql://cinder:secret_block_password@openstack-controller.example.com/cinder', +} + +class { 'cinder::api': + keystone_password => 'CINDER_PW', + keystone_user => 'cinder', + enabled => false, + manage_service => false, +} + +class { 'cinder::scheduler': + scheduler_driver => 'cinder.scheduler.simple.SimpleScheduler', + enabled => false, + manage_service => false, +} + diff --git a/cinder/lib/puppet/provider/cinder_api_paste_ini/ini_setting.rb b/cinder/lib/puppet/provider/cinder_api_paste_ini/ini_setting.rb new file mode 100644 index 000000000..26c3b528d --- /dev/null +++ b/cinder/lib/puppet/provider/cinder_api_paste_ini/ini_setting.rb @@ -0,0 +1,27 @@ +Puppet::Type.type(:cinder_api_paste_ini).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/cinder/api-paste.ini' + end + + # added for backwards compatibility with older versions of inifile + def file_path + self.class.file_path + end + +end diff --git a/cinder/lib/puppet/provider/cinder_config/ini_setting.rb b/cinder/lib/puppet/provider/cinder_config/ini_setting.rb new file mode 100644 index 000000000..6dcd95597 --- /dev/null +++ b/cinder/lib/puppet/provider/cinder_config/ini_setting.rb @@ -0,0 +1,27 @@ +Puppet::Type.type(:cinder_config).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/cinder/cinder.conf' + end + + # added for backwards compatibility with older versions of inifile + def file_path + self.class.file_path + end + +end diff --git a/cinder/lib/puppet/type/cinder_api_paste_ini.rb b/cinder/lib/puppet/type/cinder_api_paste_ini.rb new file mode 100644 index 000000000..d895b4a3c --- /dev/null +++ b/cinder/lib/puppet/type/cinder_api_paste_ini.rb @@ -0,0 +1,42 @@ +Puppet::Type.newtype(:cinder_api_paste_ini) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from /etc/cinder/api-paste.ini' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end +end diff --git a/cinder/lib/puppet/type/cinder_config.rb b/cinder/lib/puppet/type/cinder_config.rb new file mode 100644 index 000000000..62d38256b --- /dev/null +++ b/cinder/lib/puppet/type/cinder_config.rb @@ -0,0 +1,42 @@ +Puppet::Type.newtype(:cinder_config) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from /etc/cinder/cinder.conf' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end +end diff --git a/cinder/manifests/api.pp b/cinder/manifests/api.pp new file mode 100644 index 000000000..efdbfbbdb --- /dev/null +++ b/cinder/manifests/api.pp @@ -0,0 +1,214 @@ +# == Class: cinder::api +# +# Setup and configure the cinder API endpoint +# +# === Parameters +# +# [*keystone_password*] +# The password to use for authentication (keystone) +# +# [*keystone_enabled*] +# (optional) Use keystone for authentification +# Defaults to true +# +# [*keystone_tenant*] +# (optional) The tenant of the auth user +# Defaults to services +# +# [*keystone_user*] +# (optional) The name of the auth user +# Defaults to cinder +# +# [*keystone_auth_host*] +# (optional) The keystone host +# Defaults to localhost +# +# [*keystone_auth_port*] +# (optional) The keystone auth port +# Defaults to 35357 +# +# [*keystone_auth_protocol*] +# (optional) The protocol used to access the auth host +# Defaults to http. +# +# [*os_region_name*] +# (optional) Some operations require cinder to make API requests +# to Nova. This sets the keystone region to be used for these +# requests. For example, boot-from-volume. +# Defaults to undef. +# +# [*keystone_auth_admin_prefix*] +# (optional) The admin_prefix used to admin endpoint of the auth host +# This allow admin auth URIs like http://auth_host:35357/keystone. +# (where '/keystone' is the admin prefix) +# Defaults to false for empty. If defined, should be a string with a +# leading '/' and no trailing '/'. +# +# [*service_port*] +# (optional) The cinder api port +# Defaults to 5000 +# +# [*service_workers*] +# (optional) Number of cinder-api workers +# Defaults to $::processorcount +# +# [*package_ensure*] +# (optional) The state of the package +# Defaults to present +# +# [*bind_host*] +# (optional) The cinder api bind address +# Defaults to 0.0.0.0 +# +# [*enabled*] +# (optional) The state of the service +# Defaults to true +# +# [*manage_service*] +# (optional) Whether to start/stop the service +# Defaults to true +# +# [*ratelimits*] +# (optional) The state of the service +# Defaults to undef. If undefined the default ratelimiting values are used. +# +# [*ratelimits_factory*] +# (optional) Factory to use for ratelimiting +# Defaults to 'cinder.api.v1.limits:RateLimitingMiddleware.factory' +# +# [*default_volume_type*] +# (optional) default volume type to use. +# This should contain the name of the default volume type to use. +# If not configured, it produces an error when creating a volume +# without specifying a type. +# Defaults to 'false'. +class cinder::api ( + $keystone_password, + $keystone_enabled = true, + $keystone_tenant = 'services', + $keystone_user = 'cinder', + $keystone_auth_host = 'localhost', + $keystone_auth_port = '35357', + $keystone_auth_protocol = 'http', + $keystone_auth_admin_prefix = false, + $keystone_auth_uri = false, + $os_region_name = undef, + $service_port = '5000', + $service_workers = $::processorcount, + $package_ensure = 'present', + $bind_host = '0.0.0.0', + $enabled = true, + $manage_service = true, + $ratelimits = undef, + $default_volume_type = false, + $ratelimits_factory = + 'cinder.api.v1.limits:RateLimitingMiddleware.factory' +) { + + include cinder::params + + Cinder_config<||> ~> Service['cinder-api'] + Cinder_api_paste_ini<||> ~> Service['cinder-api'] + + if $::cinder::params::api_package { + Package['cinder-api'] -> Cinder_config<||> + Package['cinder-api'] -> Cinder_api_paste_ini<||> + Package['cinder-api'] -> Service['cinder-api'] + package { 'cinder-api': + ensure => $package_ensure, + name => $::cinder::params::api_package, + } + } + + if $enabled { + + Cinder_config<||> ~> Exec['cinder-manage db_sync'] + + exec { 'cinder-manage db_sync': + command => $::cinder::params::db_sync_command, + path => '/usr/bin', + user => 'cinder', + refreshonly => true, + logoutput => 'on_failure', + require => Package['cinder'], + } + if $manage_service { + $ensure = 'running' + } + } else { + if $manage_service { + $ensure = 'stopped' + } + } + + service { 'cinder-api': + ensure => $ensure, + name => $::cinder::params::api_service, + enable => $enabled, + hasstatus => true, + require => Package['cinder'], + } + + cinder_config { + 'DEFAULT/osapi_volume_listen': value => $bind_host; + 'DEFAULT/osapi_volume_workers': value => $service_workers; + } + + if $os_region_name { + cinder_config { + 'DEFAULT/os_region_name': value => $os_region_name; + } + } + + if $keystone_auth_uri { + cinder_api_paste_ini { 'filter:authtoken/auth_uri': value => $keystone_auth_uri; } + } else { + cinder_api_paste_ini { 'filter:authtoken/auth_uri': value => "${keystone_auth_protocol}://${keystone_auth_host}:${service_port}/"; } + } + + if $keystone_enabled { + cinder_config { + 'DEFAULT/auth_strategy': value => 'keystone' ; + } + cinder_api_paste_ini { + 'filter:authtoken/service_protocol': value => $keystone_auth_protocol; + 'filter:authtoken/service_host': value => $keystone_auth_host; + 'filter:authtoken/service_port': value => $service_port; + 'filter:authtoken/auth_protocol': value => $keystone_auth_protocol; + 'filter:authtoken/auth_host': value => $keystone_auth_host; + 'filter:authtoken/auth_port': value => $keystone_auth_port; + 'filter:authtoken/admin_tenant_name': value => $keystone_tenant; + 'filter:authtoken/admin_user': value => $keystone_user; + 'filter:authtoken/admin_password': value => $keystone_password, secret => true; + } + + if ($ratelimits != undef) { + cinder_api_paste_ini { + 'filter:ratelimit/paste.filter_factory': value => $ratelimits_factory; + 'filter:ratelimit/limits': value => $ratelimits; + } + } + + if $keystone_auth_admin_prefix { + validate_re($keystone_auth_admin_prefix, '^(/.+[^/])?$') + cinder_api_paste_ini { + 'filter:authtoken/auth_admin_prefix': value => $keystone_auth_admin_prefix; + } + } else { + cinder_api_paste_ini { + 'filter:authtoken/auth_admin_prefix': ensure => absent; + } + } + } + + if $default_volume_type { + cinder_config { + 'DEFAULT/default_volume_type': value => $default_volume_type; + } + } else { + cinder_config { + 'DEFAULT/default_volume_type': ensure => absent; + } + } + +} diff --git a/cinder/manifests/backend/eqlx.pp b/cinder/manifests/backend/eqlx.pp new file mode 100644 index 000000000..32ab5b427 --- /dev/null +++ b/cinder/manifests/backend/eqlx.pp @@ -0,0 +1,86 @@ +# == define: cinder::backend::eqlx +# +# Configure the Dell EqualLogic driver for cinder. +# +# === Parameters +# +# [*san_ip*] +# (required) The IP address of the Dell EqualLogic array. +# +# [*san_login*] +# (required) The account to use for issuing SSH commands. +# +# [*san_password*] +# (required) The password for the specified SSH account. +# +# [*san_thin_provision*] +# (optional) Whether or not to use thin provisioning for volumes. +# Defaults to false +# +# [*volume_backend_name*] +# (optional) The backend name. +# Defaults to the name of the resource +# +# [*eqlx_group_name*] +# (optional) The CLI prompt message without '>'. +# Defaults to 'group-0' +# +# [*eqlx_pool*] +# (optional) The pool in which volumes will be created. +# Defaults to 'default' +# +# [*eqlx_use_chap*] +# (optional) Use CHAP authentification for targets? +# Defaults to false +# +# [*eqlx_chap_login*] +# (optional) An existing CHAP account name. +# Defaults to 'chapadmin' +# +# [*eqlx_chap_password*] +# (optional) The password for the specified CHAP account name. +# Defaults to '12345' +# +# [*eqlx_cli_timeout*] +# (optional) The timeout for the Group Manager cli command execution. +# Defaults to 30 seconds +# +# [*eqlx_cli_max_retries*] +# (optional) The maximum retry count for reconnection. +# Defaults to 5 +# +define cinder::backend::eqlx ( + $san_ip, + $san_login, + $san_password, + $san_thin_provision = false, + $volume_backend_name = $name, + $eqlx_group_name = 'group-0', + $eqlx_pool = 'default', + $eqlx_use_chap = false, + $eqlx_chap_login = 'chapadmin', + $eqlx_chap_password = '12345', + $eqlx_cli_timeout = 30, + $eqlx_cli_max_retries = 5, +) { + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/volume_driver": value => 'cinder.volume.drivers.eqlx.DellEQLSanISCSIDriver'; + "${name}/san_ip": value => $san_ip; + "${name}/san_login": value => $san_login; + "${name}/san_password": value => $san_password, secret => true; + "${name}/san_thin_provision": value => $san_thin_provision; + "${name}/eqlx_group_name": value => $eqlx_group_name; + "${name}/eqlx_use_chap": value => $eqlx_use_chap; + "${name}/eqlx_cli_timeout": value => $eqlx_cli_timeout; + "${name}/eqlx_cli_max_retries": value => $eqlx_cli_max_retries; + "${name}/eqlx_pool": value => $eqlx_pool; + } + + if(str2bool($eqlx_use_chap)) { + cinder_config { + "${name}/eqlx_chap_login": value => $eqlx_chap_login; + "${name}/eqlx_chap_password": value => $eqlx_chap_password, secret => true; + } + } +} diff --git a/cinder/manifests/backend/glusterfs.pp b/cinder/manifests/backend/glusterfs.pp new file mode 100644 index 000000000..5121bb1ac --- /dev/null +++ b/cinder/manifests/backend/glusterfs.pp @@ -0,0 +1,66 @@ +# +# == Class: cinder::backend::glusterfs +# +# Configures Cinder to use GlusterFS as a volume driver +# +# === Parameters +# +# [*glusterfs_shares*] +# (required) An array of GlusterFS volume locations. +# Must be an array even if there is only one volume. +# +# [*volume_backend_name*] +# (optional) Allows for the volume_backend_name to be separate of $name. +# Defaults to: $name +# +# [*glusterfs_disk_util*] +# Removed in Icehouse. +# +# [*glusterfs_sparsed_volumes*] +# (optional) Whether or not to use sparse (thin) volumes. +# Defaults to undef which uses the driver's default of "true". +# +# [*glusterfs_mount_point_base*] +# (optional) Where to mount the Gluster volumes. +# Defaults to undef which uses the driver's default of "$state_path/mnt". +# +# [*glusterfs_shares_config*] +# (optional) The config file to store the given $glusterfs_shares. +# Defaults to '/etc/cinder/shares.conf' +# +# === Examples +# +# cinder::backend::glusterfs { 'myGluster': +# glusterfs_shares = ['192.168.1.1:/volumes'], +# } +# +define cinder::backend::glusterfs ( + $glusterfs_shares, + $volume_backend_name = $name, + $glusterfs_disk_util = false, + $glusterfs_sparsed_volumes = undef, + $glusterfs_mount_point_base = undef, + $glusterfs_shares_config = '/etc/cinder/shares.conf' +) { + + if $glusterfs_disk_util { + fail('glusterfs_disk_util is removed in Icehouse.') + } + + $content = join($glusterfs_shares, "\n") + + file { $glusterfs_shares_config: + content => "${content}\n", + require => Package['cinder'], + notify => Service['cinder-volume'] + } + + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/volume_driver": value => + 'cinder.volume.drivers.glusterfs.GlusterfsDriver'; + "${name}/glusterfs_shares_config": value => $glusterfs_shares_config; + "${name}/glusterfs_sparsed_volumes": value => $glusterfs_sparsed_volumes; + "${name}/glusterfs_mount_point_base": value => $glusterfs_mount_point_base; + } +} diff --git a/cinder/manifests/backend/iscsi.pp b/cinder/manifests/backend/iscsi.pp new file mode 100644 index 000000000..86da186cf --- /dev/null +++ b/cinder/manifests/backend/iscsi.pp @@ -0,0 +1,69 @@ +# +# Define: cinder::backend::iscsi +# Parameters: +# +# [*volume_backend_name*] +# (optional) Allows for the volume_backend_name to be separate of $name. +# Defaults to: $name +# +# +define cinder::backend::iscsi ( + $iscsi_ip_address, + $volume_backend_name = $name, + $volume_group = 'cinder-volumes', + $iscsi_helper = $::cinder::params::iscsi_helper, +) { + + include cinder::params + + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/iscsi_ip_address": value => $iscsi_ip_address; + "${name}/iscsi_helper": value => $iscsi_helper; + "${name}/volume_group": value => $volume_group; + } + + case $iscsi_helper { + 'tgtadm': { + package { 'tgt': + ensure => present, + name => $::cinder::params::tgt_package_name, + } + + if($::osfamily == 'RedHat') { + file_line { 'cinder include': + path => '/etc/tgt/targets.conf', + line => 'include /etc/cinder/volumes/*', + match => '#?include /', + require => Package['tgt'], + notify => Service['tgtd'], + } + } + + service { 'tgtd': + ensure => running, + name => $::cinder::params::tgt_service_name, + enable => true, + require => Class['cinder::volume'], + } + } + + 'lioadm': { + service { 'target': + ensure => running, + enable => true, + require => Package['targetcli'], + } + + package { 'targetcli': + ensure => present, + name => $::cinder::params::lio_package_name, + } + } + + default: { + fail("Unsupported iscsi helper: ${iscsi_helper}.") + } + } + +} diff --git a/cinder/manifests/backend/netapp.pp b/cinder/manifests/backend/netapp.pp new file mode 100644 index 000000000..4d88c7874 --- /dev/null +++ b/cinder/manifests/backend/netapp.pp @@ -0,0 +1,201 @@ +# == define: cinder::backend::netapp +# +# Configures Cinder to use the NetApp unified volume driver +# Compatible for multiple backends +# +# === Parameters +# +# [*netapp_login*] +# (required) Administrative user account name used to access the storage +# system or proxy server. +# +# [*netapp_password*] +# (required) Password for the administrative user account specified in the +# netapp_login parameter. +# +# [*netapp_server_hostname*] +# (required) The hostname (or IP address) for the storage system or proxy +# server. +# +# [*netapp_server_port*] +# (optional) The TCP port to use for communication with ONTAPI on the +# storage system. Traditionally, port 80 is used for HTTP and port 443 is +# used for HTTPS; however, this value should be changed if an alternate +# port has been configured on the storage system or proxy server. +# Defaults to 80 +# +# [*netapp_size_multiplier*] +# (optional) The quantity to be multiplied by the requested volume size to +# ensure enough space is available on the virtual storage server (Vserver) to +# fulfill the volume creation request. +# Defaults to 1.2 +# +# [*netapp_storage_family*] +# (optional) The storage family type used on the storage system; valid values +# are ontap_7mode for using Data ONTAP operating in 7-Mode or ontap_cluster +# for using clustered Data ONTAP, or eseries for NetApp E-Series. +# Defaults to ontap_cluster +# +# [*netapp_storage_protocol*] +# (optional) The storage protocol to be used on the data path with the storage +# system; valid values are iscsi or nfs. +# Defaults to nfs +# +# [*netapp_transport_type*] +# (optional) The transport protocol used when communicating with ONTAPI on the +# storage system or proxy server. Valid values are http or https. +# Defaults to http +# +# [*netapp_vfiler*] +# (optional) The vFiler unit on which provisioning of block storage volumes +# will be done. This parameter is only used by the driver when connecting to +# an instance with a storage family of Data ONTAP operating in 7-Mode and the +# storage protocol selected is iSCSI. Only use this parameter when utilizing +# the MultiStore feature on the NetApp storage system. +# Defaults to '' +# +# [*netapp_volume_list*] +# (optional) This parameter is only utilized when the storage protocol is +# configured to use iSCSI. This parameter is used to restrict provisioning to +# the specified controller volumes. Specify the value of this parameter to be +# a comma separated list of NetApp controller volume names to be used for +# provisioning. +# Defaults to '' +# +# [*netapp_vserver*] +# (optional) This parameter specifies the virtual storage server (Vserver) +# name on the storage cluster on which provisioning of block storage volumes +# should occur. If using the NFS storage protocol, this parameter is mandatory +# for storage service catalog support (utilized by Cinder volume type +# extra_specs support). If this parameter is specified, the exports belonging +# to the Vserver will only be used for provisioning in the future. Block +# storage volumes on exports not belonging to the Vserver specified by +# this parameter will continue to function normally. +# Defaults to '' +# +# [*expiry_thres_minutes*] +# (optional) This parameter specifies the threshold for last access time for +# images in the NFS image cache. When a cache cleaning cycle begins, images +# in the cache that have not been accessed in the last M minutes, where M is +# the value of this parameter, will be deleted from the cache to create free +# space on the NFS share. +# Defaults to 720 +# +# [*thres_avl_size_perc_start*] +# (optional) If the percentage of available space for an NFS share has +# dropped below the value specified by this parameter, the NFS image cache +# will be cleaned. +# Defaults to 20 +# +# [*thres_avl_size_perc_stop*] +# (optional) When the percentage of available space on an NFS share has +# reached the percentage specified by this parameter, the driver will stop +# clearing files from the NFS image cache that have not been accessed in the +# last M minutes, where M is the value of the expiry_thres_minutes parameter. +# Defaults to 60 +# +# [*nfs_shares_config*] +# (optional) File with the list of available NFS shares +# Defaults to '' +# +# [*netapp_copyoffload_tool_path*] +# (optional) This option specifies the path of the NetApp Copy Offload tool +# binary. Ensure that the binary has execute permissions set which allow the +# effective user of the cinder-volume process to execute the file. +# Defaults to '' +# +# [*netapp_controller_ips*] +# (optional) This option is only utilized when the storage family is +# configured to eseries. This option is used to restrict provisioning to the +# specified controllers. Specify the value of this option to be a comma +# separated list of controller hostnames or IP addresses to be used for +# provisioning. +# Defaults to '' +# +# [*netapp_sa_password*] +# (optional) Password for the NetApp E-Series storage array. +# Defaults to '' +# +# [*netapp_storage_pools*] +# (optional) This option is used to restrict provisioning to the specified +# storage pools. Only dynamic disk pools are currently supported. Specify the +# value of this option to be a comma separated list of disk pool names to be +# used for provisioning. +# Defaults to '' +# +# [*netapp_webservice_path*] +# (optional) This option is used to specify the path to the E-Series proxy +# application on a proxy server. The value is combined with the value of the +# netapp_transport_type, netapp_server_hostname, and netapp_server_port +# options to create the URL used by the driver to connect to the proxy +# application. +# Defaults to '/devmgr/v2' +# +# === Examples +# +# cinder::backend::netapp { 'myBackend': +# netapp_login => 'clusterAdmin', +# netapp_password => 'password', +# netapp_server_hostname => 'netapp.mycorp.com', +# netapp_server_port => '443', +# netapp_transport_type => 'https', +# netapp_vserver => 'openstack-vserver', +# } +# +# === Authors +# +# Bob Callaway +# +# === Copyright +# +# Copyright 2014 NetApp, Inc. +# +define cinder::backend::netapp ( + $netapp_login, + $netapp_password, + $netapp_server_hostname, + $volume_backend_name = $name, + $netapp_server_port = '80', + $netapp_size_multiplier = '1.2', + $netapp_storage_family = 'ontap_cluster', + $netapp_storage_protocol = 'nfs', + $netapp_transport_type = 'http', + $netapp_vfiler = '', + $netapp_volume_list = '', + $netapp_vserver = '', + $expiry_thres_minutes = '720', + $thres_avl_size_perc_start = '20', + $thres_avl_size_perc_stop = '60', + $nfs_shares_config = '', + $netapp_copyoffload_tool_path = '', + $netapp_controller_ips = '', + $netapp_sa_password = '', + $netapp_storage_pools = '', + $netapp_webservice_path = '/devmgr/v2', +) { + + cinder_config { + "${volume_backend_name}/volume_backend_name": value => $volume_backend_name; + "${volume_backend_name}/volume_driver": value => 'cinder.volume.drivers.netapp.common.NetAppDriver'; + "${volume_backend_name}/netapp_login": value => $netapp_login; + "${volume_backend_name}/netapp_password": value => $netapp_password, secret => true; + "${volume_backend_name}/netapp_server_hostname": value => $netapp_server_hostname; + "${volume_backend_name}/netapp_server_port": value => $netapp_server_port; + "${volume_backend_name}/netapp_size_multiplier": value => $netapp_size_multiplier; + "${volume_backend_name}/netapp_storage_family": value => $netapp_storage_family; + "${volume_backend_name}/netapp_storage_protocol": value => $netapp_storage_protocol; + "${volume_backend_name}/netapp_transport_type": value => $netapp_transport_type; + "${volume_backend_name}/netapp_vfiler": value => $netapp_vfiler; + "${volume_backend_name}/netapp_volume_list": value => $netapp_volume_list; + "${volume_backend_name}/netapp_vserver": value => $netapp_vserver; + "${volume_backend_name}/expiry_thres_minutes": value => $expiry_thres_minutes; + "${volume_backend_name}/thres_avl_size_perc_start": value => $thres_avl_size_perc_start; + "${volume_backend_name}/thres_avl_size_perc_stop": value => $thres_avl_size_perc_stop; + "${volume_backend_name}/nfs_shares_config": value => $nfs_shares_config; + "${volume_backend_name}/netapp_copyoffload_tool_path": value => $netapp_copyoffload_tool_path; + "${volume_backend_name}/netapp_controller_ips": value => $netapp_controller_ips; + "${volume_backend_name}/netapp_sa_password": value => $netapp_sa_password, secret => true; + "${volume_backend_name}/netapp_storage_pools": value => $netapp_storage_pools; + "${volume_backend_name}/netapp_webservice_path": value => $netapp_webservice_path; + } +} diff --git a/cinder/manifests/backend/nexenta.pp b/cinder/manifests/backend/nexenta.pp new file mode 100644 index 000000000..0124726ad --- /dev/null +++ b/cinder/manifests/backend/nexenta.pp @@ -0,0 +1,59 @@ +# == Class: cinder::backend::nexenta +# +# Setups Cinder with Nexenta volume driver. +# +# === Parameters +# +# [*nexenta_user*] +# (required) User name to connect to Nexenta SA. +# +# [*nexenta_password*] +# (required) Password to connect to Nexenta SA. +# +# [*nexenta_host*] +# (required) IP address of Nexenta SA. +# +# [*volume_backend_name*] +# (optional) Allows for the volume_backend_name to be separate of $name. +# Defaults to: $name +# +# [*nexenta_volume*] +# (optional) Pool on SA that will hold all volumes. Defaults to 'cinder'. +# +# [*nexenta_target_prefix*] +# (optional) IQN prefix for iSCSI targets. Defaults to 'iqn:'. +# +# [*nexenta_target_group_prefix*] +# (optional) Prefix for iSCSI target groups on SA. Defaults to 'cinder/'. +# +# [*nexenta_blocksize*] +# (optional) Block size for volumes. Defaults to '8k'. +# +# [*nexenta_sparse*] +# (optional) Flag to create sparse volumes. Defaults to true. +# +define cinder::backend::nexenta ( + $nexenta_user, + $nexenta_password, + $nexenta_host, + $volume_backend_name = $name, + $nexenta_volume = 'cinder', + $nexenta_target_prefix = 'iqn:', + $nexenta_target_group_prefix = 'cinder/', + $nexenta_blocksize = '8k', + $nexenta_sparse = true +) { + + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/nexenta_user": value => $nexenta_user; + "${name}/nexenta_password": value => $nexenta_password, secret => true; + "${name}/nexenta_host": value => $nexenta_host; + "${name}/nexenta_volume": value => $nexenta_volume; + "${name}/nexenta_target_prefix": value => $nexenta_target_prefix; + "${name}/nexenta_target_group_prefix": value => $nexenta_target_group_prefix; + "${name}/nexenta_blocksize": value => $nexenta_blocksize; + "${name}/nexenta_sparse": value => $nexenta_sparse; + "${name}/volume_driver": value => 'cinder.volume.drivers.nexenta.volume.NexentaDriver'; + } +} diff --git a/cinder/manifests/backend/nfs.pp b/cinder/manifests/backend/nfs.pp new file mode 100644 index 000000000..137f2697d --- /dev/null +++ b/cinder/manifests/backend/nfs.pp @@ -0,0 +1,39 @@ +# ==define cinder::backend::nfs +# +# ===Paramiters +# [*volume_backend_name*] +# (optional) Allows for the volume_backend_name to be separate of $name. +# Defaults to: $name +# +# +define cinder::backend::nfs ( + $volume_backend_name = $name, + $nfs_servers = [], + $nfs_mount_options = undef, + $nfs_disk_util = undef, + $nfs_sparsed_volumes = undef, + $nfs_mount_point_base = undef, + $nfs_shares_config = '/etc/cinder/shares.conf', + $nfs_used_ratio = '0.95', + $nfs_oversub_ratio = '1.0', +) { + + file {$nfs_shares_config: + content => join($nfs_servers, "\n"), + require => Package['cinder'], + notify => Service['cinder-volume'] + } + + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/volume_driver": value => + 'cinder.volume.drivers.nfs.NfsDriver'; + "${name}/nfs_shares_config": value => $nfs_shares_config; + "${name}/nfs_mount_options": value => $nfs_mount_options; + "${name}/nfs_disk_util": value => $nfs_disk_util; + "${name}/nfs_sparsed_volumes": value => $nfs_sparsed_volumes; + "${name}/nfs_mount_point_base": value => $nfs_mount_point_base; + "${name}/nfs_used_ratio": value => $nfs_used_ratio; + "${name}/nfs_oversub_ratio": value => $nfs_oversub_ratio; + } +} diff --git a/cinder/manifests/backend/rbd.pp b/cinder/manifests/backend/rbd.pp new file mode 100644 index 000000000..d8262ac42 --- /dev/null +++ b/cinder/manifests/backend/rbd.pp @@ -0,0 +1,109 @@ +# == define: cinder::backend::rbd +# +# Setup Cinder to use the RBD driver. +# Compatible for multiple backends +# +# === Parameters +# +# [*rbd_pool*] +# (required) Specifies the pool name for the block device driver. +# +# [*rbd_user*] +# (required) A required parameter to configure OS init scripts and cephx. +# +# [*volume_backend_name*] +# (optional) Allows for the volume_backend_name to be separate of $name. +# Defaults to: $name +# +# [*rbd_ceph_conf*] +# (optional) Path to the ceph configuration file to use +# Defaults to '/etc/ceph/ceph.conf' +# +# [*rbd_flatten_volume_from_snapshot*] +# (optional) Enable flatten volumes created from snapshots. +# Defaults to false +# +# [*rbd_secret_uuid*] +# (optional) A required parameter to use cephx. +# Defaults to false +# +# [*volume_tmp_dir*] +# (optional) Location to store temporary image files if the volume +# driver does not write them directly to the volume +# Defaults to false +# +# [*rbd_max_clone_depth*] +# (optional) Maximum number of nested clones that can be taken of a +# volume before enforcing a flatten prior to next clone. +# A value of zero disables cloning +# Defaults to '5' +# +# [*glance_api_version*] +# (optional) DEPRECATED: Use cinder::glance Class instead. +# Glance API version. (Defaults to '2') +# Setting this parameter cause a duplicate resource declaration +# with cinder::glance +# +define cinder::backend::rbd ( + $rbd_pool, + $rbd_user, + $volume_backend_name = $name, + $rbd_ceph_conf = '/etc/ceph/ceph.conf', + $rbd_flatten_volume_from_snapshot = false, + $rbd_secret_uuid = false, + $volume_tmp_dir = false, + $rbd_max_clone_depth = '5', + # DEPRECATED PARAMETERS + $glance_api_version = undef, +) { + + include cinder::params + + if $glance_api_version { + warning('The glance_api_version parameter is deprecated, use glance_api_version of cinder::glance class instead.') + } + + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/volume_driver": value => 'cinder.volume.drivers.rbd.RBDDriver'; + "${name}/rbd_ceph_conf": value => $rbd_ceph_conf; + "${name}/rbd_user": value => $rbd_user; + "${name}/rbd_pool": value => $rbd_pool; + "${name}/rbd_max_clone_depth": value => $rbd_max_clone_depth; + "${name}/rbd_flatten_volume_from_snapshot": value => $rbd_flatten_volume_from_snapshot; + } + + if $rbd_secret_uuid { + cinder_config {"${name}/rbd_secret_uuid": value => $rbd_secret_uuid;} + } else { + cinder_config {"${name}/rbd_secret_uuid": ensure => absent;} + } + + if $volume_tmp_dir { + cinder_config {"${name}/volume_tmp_dir": value => $volume_tmp_dir;} + } else { + cinder_config {"${name}/volume_tmp_dir": ensure => absent;} + } + + case $::osfamily { + 'Debian': { + $override_line = "env CEPH_ARGS=\"--id ${rbd_user}\"" + } + 'RedHat': { + $override_line = "export CEPH_ARGS=\"--id ${rbd_user}\"" + } + default: { + fail("unsuported osfamily ${::osfamily}, currently Debian and Redhat are the only supported platforms") + } + } + + # Creates an empty file if it doesn't yet exist + ensure_resource('file', $::cinder::params::ceph_init_override, {'ensure' => 'present'}) + + ensure_resource('file_line', 'set initscript env', { + line => $override_line, + path => $::cinder::params::ceph_init_override, + notify => Service['cinder-volume'] + }) + +} diff --git a/cinder/manifests/backend/san.pp b/cinder/manifests/backend/san.pp new file mode 100644 index 000000000..4ad000edd --- /dev/null +++ b/cinder/manifests/backend/san.pp @@ -0,0 +1,80 @@ +# == Class: cinder::backend::san +# +# Configures Cinder volume SAN driver. +# Parameters are particular to each volume driver. +# +# === Parameters +# +# [*volume_driver*] +# (required) Setup cinder-volume to use volume driver. +# +# [*volume_backend_name*] +# (optional) Allows for the volume_backend_name to be separate of $name. +# Defaults to: $name +# +# [*san_thin_provision*] +# (optional) Use thin provisioning for SAN volumes? Defaults to true. +# +# [*san_ip*] +# (optional) IP address of SAN controller. +# +# [*san_login*] +# (optional) Username for SAN controller. Defaults to 'admin'. +# +# [*san_password*] +# (optional) Password for SAN controller. +# +# [*san_private_key*] +# (optional) Filename of private key to use for SSH authentication. +# +# [*san_clustername*] +# (optional) Cluster name to use for creating volumes. +# +# [*san_ssh_port*] +# (optional) SSH port to use with SAN. Defaults to 22. +# +# [*san_is_local*] +# (optional) Execute commands locally instead of over SSH +# use if the volume service is running on the SAN device. +# +# [*ssh_conn_timeout*] +# (optional) SSH connection timeout in seconds. Defaults to 30. +# +# [*ssh_min_pool_conn*] +# (optional) Minimum ssh connections in the pool. +# +# [*ssh_min_pool_conn*] +# (optional) Maximum ssh connections in the pool. +# +define cinder::backend::san ( + $volume_driver, + $volume_backend_name = $name, + $san_thin_provision = true, + $san_ip = undef, + $san_login = 'admin', + $san_password = undef, + $san_private_key = undef, + $san_clustername = undef, + $san_ssh_port = 22, + $san_is_local = false, + $ssh_conn_timeout = 30, + $ssh_min_pool_conn = 1, + $ssh_max_pool_conn = 5 +) { + + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/volume_driver": value => $volume_driver; + "${name}/san_thin_provision": value => $san_thin_provision; + "${name}/san_ip": value => $san_ip; + "${name}/san_login": value => $san_login; + "${name}/san_password": value => $san_password, secret => true; + "${name}/san_private_key": value => $san_private_key; + "${name}/san_clustername": value => $san_clustername; + "${name}/san_ssh_port": value => $san_ssh_port; + "${name}/san_is_local": value => $san_is_local; + "${name}/ssh_conn_timeout": value => $ssh_conn_timeout; + "${name}/ssh_min_pool_conn": value => $ssh_min_pool_conn; + "${name}/ssh_max_pool_conn": value => $ssh_max_pool_conn; + } +} diff --git a/cinder/manifests/backend/solidfire.pp b/cinder/manifests/backend/solidfire.pp new file mode 100644 index 000000000..e5a73536c --- /dev/null +++ b/cinder/manifests/backend/solidfire.pp @@ -0,0 +1,64 @@ +# == Class: cinder::backend::solidfire +# +# Configures Cinder volume SolidFire driver. +# Parameters are particular to each volume driver. +# +# === Parameters +# +# [*volume_backend_name*] +# (optional) Allows for the volume_backend_name to be separate of $name. +# Defaults to: $name +# +# [*volume_driver*] +# (optional) Setup cinder-volume to use SolidFire volume driver. +# Defaults to 'cinder.volume.drivers.solidfire.SolidFire' +# +# [*san_ip*] +# (required) IP address of SolidFire clusters MVIP. +# +# [*san_login*] +# (required) Username for SolidFire admin account. +# +# [*san_password*] +# (required) Password for SolidFire admin account. +# +# [*sf_emulate_512*] +# (optional) Use 512 byte emulation for volumes. +# Defaults to True +# +# [*sf_allow_tenant_qos*] +# (optional) Allow tenants to specify QoS via volume metadata. +# Defaults to False +# +# [*sf_account_prefix*] +# (optional) Prefix to use when creating tenant accounts on SolidFire Cluster. +# Defaults to None, so account name is simply the tenant-uuid +# +# [*sf_api_port*] +# (optional) Port ID to use to connect to SolidFire API. +# Defaults to 443 +# +define cinder::backend::solidfire( + $san_ip, + $san_login, + $san_password, + $volume_backend_name = $name, + $volume_driver = 'cinder.volume.drivers.solidfire.SolidFire', + $sf_emulate_512 = true, + $sf_allow_tenant_qos = false, + $sf_account_prefix = '', + $sf_api_port = '443' +) { + + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/volume_driver": value => $volume_driver; + "${name}/san_ip": value => $san_ip; + "${name}/san_login": value => $san_login; + "${name}/san_password": value => $san_password, secret => true; + "${name}/sf_emulate_512": value => $sf_emulate_512; + "${name}/sf_allow_tenant_qos": value => $sf_allow_tenant_qos; + "${name}/sf_account_prefix": value => $sf_account_prefix; + "${name}/sf_api_port": value => $sf_api_port; + } +} diff --git a/cinder/manifests/backend/vmdk.pp b/cinder/manifests/backend/vmdk.pp new file mode 100644 index 000000000..6ce4dba0c --- /dev/null +++ b/cinder/manifests/backend/vmdk.pp @@ -0,0 +1,87 @@ +# == define: cinder::backend::vmdk +# +# Configure the VMware VMDK driver for cinder. +# +# === Parameters +# +# [*host_ip*] +# The IP address of the VMware vCenter server. +# +# [*host_username*] +# The username for connection to VMware vCenter server. +# +# [*host_password*] +# The password for connection to VMware vCenter server. +# +# [*volume_backend_name*] +# Used to set the volume_backend_name in multiple backends. +# Defaults to $name as passed in the title. +# +# [*api_retry_count*] +# (optional) The number of times we retry on failures, +# e.g., socket error, etc. +# Defaults to 10. +# +# [*$max_object_retrieval*] +# (optional) The maximum number of ObjectContent data objects that should +# be returned in a single result. A positive value will cause +# the operation to suspend the retrieval when the count of +# objects reaches the specified maximum. The server may still +# limit the count to something less than the configured value. +# Any remaining objects may be retrieved with additional requests. +# Defaults to 100. +# +# [*task_poll_interval*] +# (optional) The interval in seconds used for polling of remote tasks. +# Defaults to 5. +# +# [*image_transfer_timeout_secs*] +# (optional) The timeout in seconds for VMDK volume transfer between Cinder and Glance. +# Defaults to 7200. +# +# [*wsdl_location*] +# (optional) VIM Service WSDL Location e.g +# http:///vimService.wsdl. Optional over-ride to +# default location for bug work-arounds. +# Defaults to None. +# +# [*volume_folder*] +# (optional) The name for the folder in the VC datacenter that will contain cinder volumes. +# Defaults to 'cinder-volumes'. +# +define cinder::backend::vmdk ( + $host_ip, + $host_username, + $host_password, + $volume_backend_name = $name, + $volume_folder = 'cinder-volumes', + $api_retry_count = 10, + $max_object_retrieval = 100, + $task_poll_interval = 5, + $image_transfer_timeout_secs = 7200, + $wsdl_location = undef + ) { + + cinder_config { + "${name}/volume_backend_name": value => $volume_backend_name; + "${name}/volume_driver": value => 'cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver'; + "${name}/vmware_host_ip": value => $host_ip; + "${name}/vmware_host_username": value => $host_username; + "${name}/vmware_host_password": value => $host_password, secret => true; + "${name}/vmware_volume_folder": value => $volume_folder; + "${name}/vmware_api_retry_count": value => $api_retry_count; + "${name}/vmware_max_object_retrieval": value => $max_object_retrieval; + "${name}/vmware_task_poll_interval": value => $task_poll_interval; + "${name}/vmware_image_transfer_timeout_secs": value => $image_transfer_timeout_secs; + } + + if $wsdl_location { + cinder_config { + "${name}/vmware_wsdl_location": value => $wsdl_location; + } + } + + package { 'python-suds': + ensure => present + } +} diff --git a/cinder/manifests/backends.pp b/cinder/manifests/backends.pp new file mode 100644 index 000000000..7c6971938 --- /dev/null +++ b/cinder/manifests/backends.pp @@ -0,0 +1,28 @@ +# == Class: cinder::backends +# +# Class to set the enabled_backends list +# +# === Parameters +# +# [*enabled_backends*] +# (required) a list of ini sections to enable. +# This should contain names used in ceph::backend::* resources. +# Example: ['volume1', 'volume2', 'sata3'] +# +# Author: Andrew Woodward +class cinder::backends ( + $enabled_backends = undef, + # DEPRECATED + $default_volume_type = false + ){ + + # Maybe this could be extented to dynamicly find the enabled names + cinder_config { + 'DEFAULT/enabled_backends': value => join($enabled_backends, ','); + } + + if $default_volume_type { + fail('The default_volume_type parameter is deprecated in this class, you should declare it in cinder::api.') + } + +} diff --git a/cinder/manifests/backup.pp b/cinder/manifests/backup.pp new file mode 100644 index 000000000..a8c575a09 --- /dev/null +++ b/cinder/manifests/backup.pp @@ -0,0 +1,84 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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: cinder::backup +# +# Setup Cinder backup service +# +# === Parameters +# +# [*backup_topic*] +# (optional) The topic volume backup nodes listen on. +# Defaults to 'cinder-backup' +# +# [*backup_manager*] +# (optional) Full class name for the Manager for volume backup. +# Defaults to 'cinder.backup.manager.BackupManager' +# +# [*backup_api_class*] +# (optional) The full class name of the volume backup API class. +# Defaults to 'cinder.backup.api.API' +# +# [*backup_name_template*] +# (optional) Template string to be used to generate backup names. +# Defaults to 'backup-%s' +# + +class cinder::backup ( + $enabled = true, + $package_ensure = 'present', + $backup_topic = 'cinder-backup', + $backup_manager = 'cinder.backup.manager.BackupManager', + $backup_api_class = 'cinder.backup.api.API', + $backup_name_template = 'backup-%s' +) { + + include cinder::params + + Cinder_config<||> ~> Service['cinder-backup'] + + if $::cinder::params::backup_package { + Package['cinder-backup'] -> Cinder_config<||> + Package['cinder-backup'] -> Service['cinder-backup'] + package { 'cinder-backup': + ensure => $package_ensure, + name => $::cinder::params::backup_package, + } + } + + if $enabled { + $ensure = 'running' + } else { + $ensure = 'stopped' + } + + service { 'cinder-backup': + ensure => $ensure, + name => $::cinder::params::backup_service, + enable => $enabled, + hasstatus => true, + require => Package['cinder'], + } + + cinder_config { + 'DEFAULT/backup_topic': value => $backup_topic; + 'DEFAULT/backup_manager': value => $backup_manager; + 'DEFAULT/backup_api_class': value => $backup_api_class; + 'DEFAULT/backup_name_template': value => $backup_name_template; + } + +} diff --git a/cinder/manifests/backup/ceph.pp b/cinder/manifests/backup/ceph.pp new file mode 100644 index 000000000..9ac208ab8 --- /dev/null +++ b/cinder/manifests/backup/ceph.pp @@ -0,0 +1,76 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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: cinder::backup::ceph +# +# Setup Cinder to backup volumes into Ceph +# +# === Parameters +# +# [*backup_ceph_conf*] +# (optional) Ceph config file to use. +# Should be a valid ceph configuration file +# Defaults to '/etc/ceph/ceph.conf' +# +# [*backup_ceph_user*] +# (optional) The Ceph user to connect with. +# Should be a valid user +# Defaults to 'cinder' +# +# [*backup_ceph_chunk_size*] +# (optional) The chunk size in bytes that a backup will be broken into +# before transfer to backup store. +# Should be a valid integer +# Defaults to '134217728' +# +# [*backup_ceph_pool*] +# (optional) The Ceph pool to backup to. +# Should be a valid ceph pool +# Defaults to 'backups' +# +# [*backup_ceph_stripe_unit*] +# (optional) RBD stripe unit to use when creating a backup image. +# Should be a valid integer +# Defaults to '0' +# +# [*backup_ceph_stripe_count*] +# (optional) RBD stripe count to use when creating a backup image. +# Should be a valid integer +# Defaults to '0' +# + +class cinder::backup::ceph ( + $backup_driver = 'cinder.backup.driver.ceph', + $backup_ceph_conf = '/etc/ceph/ceph.conf', + $backup_ceph_user = 'cinder', + $backup_ceph_chunk_size = '134217728', + $backup_ceph_pool = 'backups', + $backup_ceph_stripe_unit = '0', + $backup_ceph_stripe_count = '0' +) { + + cinder_config { + 'DEFAULT/backup_driver': value => $backup_driver; + 'DEFAULT/backup_ceph_conf': value => $backup_ceph_conf; + 'DEFAULT/backup_ceph_user': value => $backup_ceph_user; + 'DEFAULT/backup_ceph_chunk_size': value => $backup_ceph_chunk_size; + 'DEFAULT/backup_ceph_pool': value => $backup_ceph_pool; + 'DEFAULT/backup_ceph_stripe_unit': value => $backup_ceph_stripe_unit; + 'DEFAULT/backup_ceph_stripe_count': value => $backup_ceph_stripe_count; + } + +} diff --git a/cinder/manifests/backup/swift.pp b/cinder/manifests/backup/swift.pp new file mode 100644 index 000000000..5b1fedfb8 --- /dev/null +++ b/cinder/manifests/backup/swift.pp @@ -0,0 +1,64 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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: cinder::backup::swift +# +# Setup Cinder to backup volumes into Swift +# +# === Parameters +# +# [*backup_swift_url*] +# (optional) The URL of the Swift endpoint. +# Should be a valid Swift URL +# Defaults to 'http://localhost:8080/v1/AUTH_' +# +# [*backup_swift_container*] +# (optional) The default Swift container to use. +# Defaults to 'volumes_backup' +# +# [*backup_swift_object_size*] +# (optional) The size in bytes of Swift backup objects. +# Defaults to '52428800' +# +# [*backup_swift_retry_attempts*] +# (optional) The number of retries to make for Swift operations. +# Defaults to '3' +# +# [*backup_swift_retry_backoff*] +# (optional) The backoff time in seconds between Swift retries. +# Defaults to '2' +# + +class cinder::backup::swift ( + $backup_driver = 'cinder.backup.drivers.swift', + $backup_swift_url = 'http://localhost:8080/v1/AUTH_', + $backup_swift_container = 'volumes_backup', + $backup_swift_object_size = '52428800', + $backup_swift_retry_attempts = '3', + $backup_swift_retry_backoff = '2' +) { + + cinder_config { + 'DEFAULT/backup_driver': value => $backup_driver; + 'DEFAULT/backup_swift_url': value => $backup_swift_url; + 'DEFAULT/backup_swift_container': value => $backup_swift_container; + 'DEFAULT/backup_swift_object_size': value => $backup_swift_object_size; + 'DEFAULT/backup_swift_retry_attempts': value => $backup_swift_retry_attempts; + 'DEFAULT/backup_swift_retry_backoff': value => $backup_swift_retry_backoff; + } + +} diff --git a/cinder/manifests/ceilometer.pp b/cinder/manifests/ceilometer.pp new file mode 100644 index 000000000..813ea687d --- /dev/null +++ b/cinder/manifests/ceilometer.pp @@ -0,0 +1,22 @@ +# == Class: cinder::ceilometer +# +# Setup Cinder to enable ceilometer can retrieve volume samples +# Ref: http://docs.openstack.org/developer/ceilometer/install/manual.html +# +# === Parameters +# +# [*notification_driver*] +# (option) Driver or drivers to handle sending notifications. +# Notice: rabbit_notifier has been deprecated in Grizzly, use rpc_notifier instead. +# + + +class cinder::ceilometer ( + $notification_driver = 'cinder.openstack.common.notifier.rpc_notifier' +) { + + cinder_config { + 'DEFAULT/notification_driver': value => $notification_driver; + } +} + diff --git a/cinder/manifests/client.pp b/cinder/manifests/client.pp new file mode 100644 index 000000000..1b6ad8226 --- /dev/null +++ b/cinder/manifests/client.pp @@ -0,0 +1,20 @@ +# == Class: cinder::client +# +# Installs Cinder python client. +# +# === Parameters +# +# [*ensure*] +# Ensure state for package. Defaults to 'present'. +# +class cinder::client( + $package_ensure = 'present' +) { + + include cinder::params + + package { 'python-cinderclient': + ensure => $package_ensure, + name => $::cinder::params::client_package, + } +} diff --git a/cinder/manifests/config.pp b/cinder/manifests/config.pp new file mode 100644 index 000000000..0fa29a3e9 --- /dev/null +++ b/cinder/manifests/config.pp @@ -0,0 +1,39 @@ +# == Class: cinder::config +# +# This class is used to manage arbitrary cinder configurations. +# +# === Parameters +# +# [*xxx_config*] +# (optional) Allow configuration of arbitrary cinder configurations. +# The value is an hash of xxx_config resources. Example: +# { 'DEFAULT/foo' => { value => 'fooValue'}, +# 'DEFAULT/bar' => { value => 'barValue'} +# } +# +# In yaml format, Example: +# xxx_config: +# DEFAULT/foo: +# value: fooValue +# DEFAULT/bar: +# value: barValue +# +# [**cinder_config**] +# (optional) Allow configuration of cinder.conf configurations. +# +# [**api_paste_ini_config**] +# (optional) Allow configuration of /etc/cinder/api-paste.ini configurations. +# +# NOTE: The configuration MUST NOT be already handled by this module +# or Puppet catalog compilation will fail with duplicate resources. +# +class cinder::config ( + $cinder_config = {}, + $api_paste_ini_config = {}, +) { + validate_hash($cinder_config) + validate_hash($api_paste_ini_config) + + create_resources('cinder_config', $cinder_config) + create_resources('cinder_api_paste_ini', $api_paste_ini_config) +} diff --git a/cinder/manifests/db/mysql.pp b/cinder/manifests/db/mysql.pp new file mode 100644 index 000000000..21259682d --- /dev/null +++ b/cinder/manifests/db/mysql.pp @@ -0,0 +1,61 @@ +# The cinder::db::mysql class creates a MySQL database for cinder. +# It must be used on the MySQL server +# +# == Parameters +# +# [*password*] +# password to connect to the database. Mandatory. +# +# [*dbname*] +# name of the database. Optional. Defaults to cinder. +# +# [*user*] +# user to connect to the database. Optional. Defaults to cinder. +# +# [*host*] +# the default source host user is allowed to connect from. +# Optional. Defaults to 'localhost' +# +# [*allowed_hosts*] +# other hosts the user is allowd to connect from. +# Optional. Defaults to undef. +# +# [*charset*] +# the database charset. Optional. Defaults to 'utf8' +# +# [*collate*] +# the database collation. Optional. Defaults to 'utf8_unicode_ci' +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +class cinder::db::mysql ( + $password, + $dbname = 'cinder', + $user = 'cinder', + $host = '127.0.0.1', + $allowed_hosts = undef, + $charset = 'utf8', + $collate = 'utf8_unicode_ci', + $cluster_id = 'localzone', + $mysql_module = undef, +) { + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + validate_string($password) + + ::openstacklib::db::mysql { 'cinder': + user => $user, + password_hash => mysql_password($password), + dbname => $dbname, + host => $host, + charset => $charset, + collate => $collate, + allowed_hosts => $allowed_hosts, + } + + ::Openstacklib::Db::Mysql['cinder'] ~> Exec<| title == 'cinder-manage db_sync' |> +} diff --git a/cinder/manifests/db/postgresql.pp b/cinder/manifests/db/postgresql.pp new file mode 100644 index 000000000..52aa15bb7 --- /dev/null +++ b/cinder/manifests/db/postgresql.pp @@ -0,0 +1,21 @@ +# +# Class that configures postgresql for cinder +# +# Requires the Puppetlabs postgresql module. +class cinder::db::postgresql( + $password, + $dbname = 'cinder', + $user = 'cinder' +) { + + require postgresql::python + + Postgresql::Db[$dbname] ~> Exec<| title == 'cinder-manage db_sync' |> + Package['python-psycopg2'] -> Exec<| title == 'cinder-manage db_sync' |> + + postgresql::db { $dbname: + user => $user, + password => $password, + } + +} diff --git a/cinder/manifests/db/sync.pp b/cinder/manifests/db/sync.pp new file mode 100644 index 000000000..942f25213 --- /dev/null +++ b/cinder/manifests/db/sync.pp @@ -0,0 +1,14 @@ +# +class cinder::db::sync { + + include cinder::params + + exec { 'cinder-manage db_sync': + command => $::cinder::params::db_sync_command, + path => '/usr/bin', + user => 'cinder', + refreshonly => true, + require => [File[$::cinder::params::cinder_conf], Class['cinder']], + logoutput => 'on_failure', + } +} diff --git a/cinder/manifests/glance.pp b/cinder/manifests/glance.pp new file mode 100644 index 000000000..a9f570784 --- /dev/null +++ b/cinder/manifests/glance.pp @@ -0,0 +1,82 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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: cinder::glance +# +# Glance drive Cinder as a block storage backend to store image data. +# +# === Parameters +# +# [*glance_api_servers*] +# (optional) A list of the glance api servers available to cinder. +# Should be an array with [hostname|ip]:port +# Defaults to undef +# +# [*glance_api_version*] +# (optional) Glance API version. +# Should be 1 or 2 +# Defaults to 2 (current version) +# +# [*glance_num_retries*] +# (optional) Number retries when downloading an image from glance. +# Defaults to 0 +# +# [*glance_api_insecure*] +# (optional) Allow to perform insecure SSL (https) requests to glance. +# Defaults to false +# +# [*glance_api_ssl_compression*] +# (optional) Whether to attempt to negotiate SSL layer compression when +# using SSL (https) requests. Set to False to disable SSL +# layer compression. In some cases disabling this may improve +# data throughput, eg when high network bandwidth is available +# and you are using already compressed image formats such as qcow2. +# Defaults to false +# +# [*glance_request_timeout*] +# (optional) http/https timeout value for glance operations. +# Defaults to undef +# + +class cinder::glance ( + $glance_api_servers = undef, + $glance_api_version = '2', + $glance_num_retries = '0', + $glance_api_insecure = false, + $glance_api_ssl_compression = false, + $glance_request_timeout = undef +) { + + if is_array($glance_api_servers) { + cinder_config { + 'DEFAULT/glance_api_servers': value => join($glance_api_servers, ','); + } + } elsif is_string($glance_api_servers) { + cinder_config { + 'DEFAULT/glance_api_servers': value => $glance_api_servers; + } + } + + cinder_config { + 'DEFAULT/glance_api_version': value => $glance_api_version; + 'DEFAULT/glance_num_retries': value => $glance_num_retries; + 'DEFAULT/glance_api_insecure': value => $glance_api_insecure; + 'DEFAULT/glance_api_ssl_compression': value => $glance_api_ssl_compression; + 'DEFAULT/glance_request_timeout': value => $glance_request_timeout; + } + +} diff --git a/cinder/manifests/init.pp b/cinder/manifests/init.pp new file mode 100644 index 000000000..717b1f41d --- /dev/null +++ b/cinder/manifests/init.pp @@ -0,0 +1,358 @@ +# +# == Parameters +# [database_connection] +# Url used to connect to database. +# (Optional) Defaults to +# 'sqlite:////var/lib/cinder/cinder.sqlite' +# +# [database_idle_timeout] +# Timeout when db connections should be reaped. +# (Optional) Defaults to 3600. +# +# [*rabbit_use_ssl*] +# (optional) Connect over SSL for RabbitMQ +# Defaults to false +# +# [*kombu_ssl_ca_certs*] +# (optional) SSL certification authority file (valid only if SSL enabled). +# Defaults to undef +# +# [*kombu_ssl_certfile*] +# (optional) SSL cert file (valid only if SSL enabled). +# Defaults to undef +# +# [*kombu_ssl_keyfile*] +# (optional) SSL key file (valid only if SSL enabled). +# Defaults to undef +# +# [*kombu_ssl_version*] +# (optional) SSL version to use (valid only if SSL enabled). +# Valid values are TLSv1, SSLv23 and SSLv3. SSLv2 may be +# available on some distributions. +# Defaults to 'SSLv3' +# +# [amqp_durable_queues] +# Use durable queues in amqp. +# (Optional) Defaults to false. +# +# [use_syslog] +# Use syslog for logging. +# (Optional) Defaults to false. +# +# [log_facility] +# Syslog facility to receive log lines. +# (Optional) Defaults to LOG_USER. +# +# [*log_dir*] +# (optional) Directory where logs should be stored. +# If set to boolean false, it will not log to any directory. +# Defaults to '/var/log/cinder' +# +# [*use_ssl*] +# (optional) Enable SSL on the API server +# Defaults to false, not set +# +# [*cert_file*] +# (optinal) Certificate file to use when starting API server securely +# Defaults to false, not set +# +# [*key_file*] +# (optional) Private key file to use when starting API server securely +# Defaults to false, not set +# +# [*ca_file*] +# (optional) CA certificate file to use to verify connecting clients +# Defaults to false, not set_ +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +# [*storage_availability_zone*] +# (optional) Availability zone of the node. +# Defaults to 'nova' +# +# [*default_availability_zone*] +# (optional) Default availability zone for new volumes. +# If not set, the storage_availability_zone option value is used as +# the default for new volumes. +# Defaults to false +# +# [sql_connection] +# DEPRECATED +# [sql_idle_timeout] +# DEPRECATED +# +class cinder ( + $database_connection = 'sqlite:////var/lib/cinder/cinder.sqlite', + $database_idle_timeout = '3600', + $rpc_backend = 'cinder.openstack.common.rpc.impl_kombu', + $control_exchange = 'openstack', + $rabbit_host = '127.0.0.1', + $rabbit_port = 5672, + $rabbit_hosts = false, + $rabbit_virtual_host = '/', + $rabbit_userid = 'guest', + $rabbit_password = false, + $rabbit_use_ssl = false, + $kombu_ssl_ca_certs = undef, + $kombu_ssl_certfile = undef, + $kombu_ssl_keyfile = undef, + $kombu_ssl_version = 'SSLv3', + $amqp_durable_queues = false, + $qpid_hostname = 'localhost', + $qpid_port = '5672', + $qpid_username = 'guest', + $qpid_password = false, + $qpid_sasl_mechanisms = false, + $qpid_reconnect = true, + $qpid_reconnect_timeout = 0, + $qpid_reconnect_limit = 0, + $qpid_reconnect_interval_min = 0, + $qpid_reconnect_interval_max = 0, + $qpid_reconnect_interval = 0, + $qpid_heartbeat = 60, + $qpid_protocol = 'tcp', + $qpid_tcp_nodelay = true, + $package_ensure = 'present', + $use_ssl = false, + $ca_file = false, + $cert_file = false, + $key_file = false, + $api_paste_config = '/etc/cinder/api-paste.ini', + $use_syslog = false, + $log_facility = 'LOG_USER', + $log_dir = '/var/log/cinder', + $verbose = false, + $debug = false, + $storage_availability_zone = 'nova', + $default_availability_zone = false, + # DEPRECATED PARAMETERS + $mysql_module = undef, + $sql_connection = undef, + $sql_idle_timeout = undef, +) { + + include cinder::params + + Package['cinder'] -> Cinder_config<||> + Package['cinder'] -> Cinder_api_paste_ini<||> + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + if $sql_connection { + warning('The sql_connection parameter is deprecated, use database_connection instead.') + $database_connection_real = $sql_connection + } else { + $database_connection_real = $database_connection + } + + if $sql_idle_timeout { + warning('The sql_idle_timeout parameter is deprecated, use database_idle_timeout instead.') + $database_idle_timeout_real = $sql_idle_timeout + } else { + $database_idle_timeout_real = $database_idle_timeout + } + + if $use_ssl { + if !$cert_file { + fail('The cert_file parameter is required when use_ssl is set to true') + } + if !$key_file { + fail('The key_file parameter is required when use_ssl is set to true') + } + } + + if $rabbit_use_ssl { + if !$kombu_ssl_ca_certs { + fail('The kombu_ssl_ca_certs parameter is required when rabbit_use_ssl is set to true') + } + if !$kombu_ssl_certfile { + fail('The kombu_ssl_certfile parameter is required when rabbit_use_ssl is set to true') + } + if !$kombu_ssl_keyfile { + fail('The kombu_ssl_keyfile parameter is required when rabbit_use_ssl is set to true') + } + } + + # this anchor is used to simplify the graph between cinder components by + # allowing a resource to serve as a point where the configuration of cinder begins + anchor { 'cinder-start': } + + package { 'cinder': + ensure => $package_ensure, + name => $::cinder::params::package_name, + require => Anchor['cinder-start'], + } + + file { $::cinder::params::cinder_conf: + ensure => present, + owner => 'cinder', + group => 'cinder', + mode => '0600', + require => Package['cinder'], + } + + file { $::cinder::params::cinder_paste_api_ini: + ensure => present, + owner => 'cinder', + group => 'cinder', + mode => '0600', + require => Package['cinder'], + } + + if $rpc_backend == 'cinder.openstack.common.rpc.impl_kombu' { + + if ! $rabbit_password { + fail('Please specify a rabbit_password parameter.') + } + + cinder_config { + 'DEFAULT/rabbit_password': value => $rabbit_password, secret => true; + 'DEFAULT/rabbit_userid': value => $rabbit_userid; + 'DEFAULT/rabbit_virtual_host': value => $rabbit_virtual_host; + 'DEFAULT/rabbit_use_ssl': value => $rabbit_use_ssl; + 'DEFAULT/control_exchange': value => $control_exchange; + 'DEFAULT/amqp_durable_queues': value => $amqp_durable_queues; + } + + if $rabbit_hosts { + cinder_config { 'DEFAULT/rabbit_hosts': value => join($rabbit_hosts, ',') } + cinder_config { 'DEFAULT/rabbit_ha_queues': value => true } + } else { + cinder_config { 'DEFAULT/rabbit_host': value => $rabbit_host } + cinder_config { 'DEFAULT/rabbit_port': value => $rabbit_port } + cinder_config { 'DEFAULT/rabbit_hosts': value => "${rabbit_host}:${rabbit_port}" } + cinder_config { 'DEFAULT/rabbit_ha_queues': value => false } + } + + if $rabbit_use_ssl { + cinder_config { + 'DEFAULT/kombu_ssl_ca_certs': value => $kombu_ssl_ca_certs; + 'DEFAULT/kombu_ssl_certfile': value => $kombu_ssl_certfile; + 'DEFAULT/kombu_ssl_keyfile': value => $kombu_ssl_keyfile; + 'DEFAULT/kombu_ssl_version': value => $kombu_ssl_version; + } + } else { + cinder_config { + 'DEFAULT/kombu_ssl_ca_certs': ensure => absent; + 'DEFAULT/kombu_ssl_certfile': ensure => absent; + 'DEFAULT/kombu_ssl_keyfile': ensure => absent; + 'DEFAULT/kombu_ssl_version': ensure => absent; + } + } + + } + + if $rpc_backend == 'cinder.openstack.common.rpc.impl_qpid' { + + if ! $qpid_password { + fail('Please specify a qpid_password parameter.') + } + + cinder_config { + 'DEFAULT/qpid_hostname': value => $qpid_hostname; + 'DEFAULT/qpid_port': value => $qpid_port; + 'DEFAULT/qpid_username': value => $qpid_username; + 'DEFAULT/qpid_password': value => $qpid_password, secret => true; + 'DEFAULT/qpid_reconnect': value => $qpid_reconnect; + 'DEFAULT/qpid_reconnect_timeout': value => $qpid_reconnect_timeout; + 'DEFAULT/qpid_reconnect_limit': value => $qpid_reconnect_limit; + 'DEFAULT/qpid_reconnect_interval_min': value => $qpid_reconnect_interval_min; + 'DEFAULT/qpid_reconnect_interval_max': value => $qpid_reconnect_interval_max; + 'DEFAULT/qpid_reconnect_interval': value => $qpid_reconnect_interval; + 'DEFAULT/qpid_heartbeat': value => $qpid_heartbeat; + 'DEFAULT/qpid_protocol': value => $qpid_protocol; + 'DEFAULT/qpid_tcp_nodelay': value => $qpid_tcp_nodelay; + 'DEFAULT/amqp_durable_queues': value => $amqp_durable_queues; + } + + if is_array($qpid_sasl_mechanisms) { + cinder_config { + 'DEFAULT/qpid_sasl_mechanisms': value => join($qpid_sasl_mechanisms, ' '); + } + } elsif $qpid_sasl_mechanisms { + cinder_config { + 'DEFAULT/qpid_sasl_mechanisms': value => $qpid_sasl_mechanisms; + } + } else { + cinder_config { + 'DEFAULT/qpid_sasl_mechanisms': ensure => absent; + } + } + } + + if ! $default_availability_zone { + $default_availability_zone_real = $storage_availability_zone + } else { + $default_availability_zone_real = $default_availability_zone + } + + cinder_config { + 'database/connection': value => $database_connection_real, secret => true; + 'database/idle_timeout': value => $database_idle_timeout_real; + 'DEFAULT/verbose': value => $verbose; + 'DEFAULT/debug': value => $debug; + 'DEFAULT/api_paste_config': value => $api_paste_config; + 'DEFAULT/rpc_backend': value => $rpc_backend; + 'DEFAULT/storage_availability_zone': value => $storage_availability_zone; + 'DEFAULT/default_availability_zone': value => $default_availability_zone_real; + } + + if($database_connection_real =~ /mysql:\/\/\S+:\S+@\S+\/\S+/) { + require 'mysql::bindings' + require 'mysql::bindings::python' + } elsif($database_connection_real =~ /postgresql:\/\/\S+:\S+@\S+\/\S+/) { + + } elsif($database_connection_real =~ /sqlite:\/\//) { + + } else { + fail("Invalid db connection ${database_connection_real}") + } + + if $log_dir { + cinder_config { + 'DEFAULT/log_dir': value => $log_dir; + } + } else { + cinder_config { + 'DEFAULT/log_dir': ensure => absent; + } + } + + # SSL Options + if $use_ssl { + cinder_config { + 'DEFAULT/ssl_cert_file' : value => $cert_file; + 'DEFAULT/ssl_key_file' : value => $key_file; + } + if $ca_file { + cinder_config { 'DEFAULT/ssl_ca_file' : + value => $ca_file, + } + } else { + cinder_config { 'DEFAULT/ssl_ca_file' : + ensure => absent, + } + } + } else { + cinder_config { + 'DEFAULT/ssl_cert_file' : ensure => absent; + 'DEFAULT/ssl_key_file' : ensure => absent; + 'DEFAULT/ssl_ca_file' : ensure => absent; + } + } + + if $use_syslog { + cinder_config { + 'DEFAULT/use_syslog': value => true; + 'DEFAULT/syslog_log_facility': value => $log_facility; + } + } else { + cinder_config { + 'DEFAULT/use_syslog': value => false; + } + } + +} diff --git a/cinder/manifests/keystone/auth.pp b/cinder/manifests/keystone/auth.pp new file mode 100644 index 000000000..1621d1bdb --- /dev/null +++ b/cinder/manifests/keystone/auth.pp @@ -0,0 +1,137 @@ +# == Class: cinder::keystone::auth +# +# Configures Cinder user, service and endpoint in Keystone. +# +# === Parameters +# +# [*password*] +# Password for Cinder user. Required. +# +# [*email*] +# Email for Cinder user. Optional. Defaults to 'cinder@localhost'. +# +# [*auth_name*] +# Username for Cinder service. Optional. Defaults to 'cinder'. +# +# [*auth_name_v2*] +# Username for Cinder v2 service. Optional. Defaults to 'cinder2'. +# +# [*configure_endpoint*] +# Should Cinder endpoint be configured? Optional. Defaults to 'true'. +# API v1 endpoint should be enabled in Icehouse for compatibility with Nova. +# +# [*configure_endpoint_v2*] +# Should Cinder v2 endpoint be configured? Optional. Defaults to 'true'. +# +# [*configure_user*] +# Should the service user be configured? Optional. Defaults to 'true'. +# +# [*configure_user_role*] +# Should the admin role be configured for the service user? +# Optional. Defaults to 'true'. +# +# [*service_type*] +# Type of service. Optional. Defaults to 'volume'. +# +# [*service_type_v2*] +# Type of API v2 service. Optional. Defaults to 'volume2'. +# +# [*public_address*] +# Public address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*admin_address*] +# Admin address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*internal_address*] +# Internal address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*port*] +# Port for endpoint. Optional. Defaults to '8776'. +# +# [*volume_version*] +# Cinder API version. Optional. Defaults to 'v1'. +# +# [*region*] +# Region for endpoint. Optional. Defaults to 'RegionOne'. +# +# [*tenant*] +# Tenant for Cinder user. Optional. Defaults to 'services'. +# +# [*public_protocol*] +# Protocol for public endpoint. Optional. Defaults to 'http'. +# +# [*internal_protocol*] +# Protocol for internal endpoint. Optional. Defaults to 'http'. +# +# [*admin_protocol*] +# Protocol for admin endpoint. Optional. Defaults to 'http'. +# +class cinder::keystone::auth ( + $password, + $auth_name = 'cinder', + $auth_name_v2 = 'cinderv2', + $email = 'cinder@localhost', + $tenant = 'services', + $configure_endpoint = true, + $configure_endpoint_v2 = true, + $configure_user = true, + $configure_user_role = true, + $service_type = 'volume', + $service_type_v2 = 'volumev2', + $public_address = '127.0.0.1', + $admin_address = '127.0.0.1', + $internal_address = '127.0.0.1', + $port = '8776', + $volume_version = 'v1', + $region = 'RegionOne', + $public_protocol = 'http', + $admin_protocol = 'http', + $internal_protocol = 'http' +) { + + if $configure_user { + keystone_user { $auth_name: + ensure => present, + password => $password, + email => $email, + tenant => $tenant, + } + } + + if $configure_user_role { + Keystone_user_role["${auth_name}@${tenant}"] ~> Service <| name == 'cinder-api' |> + + keystone_user_role { "${auth_name}@${tenant}": + ensure => present, + roles => 'admin', + } + } + + keystone_service { $auth_name: + ensure => present, + type => $service_type, + description => 'Cinder Service', + } + keystone_service { $auth_name_v2: + ensure => present, + type => $service_type_v2, + description => 'Cinder Service v2', + } + + if $configure_endpoint { + keystone_endpoint { "${region}/${auth_name}": + ensure => present, + public_url => "${public_protocol}://${public_address}:${port}/${volume_version}/%(tenant_id)s", + admin_url => "${admin_protocol}://${admin_address}:${port}/${volume_version}/%(tenant_id)s", + internal_url => "${internal_protocol}://${internal_address}:${port}/${volume_version}/%(tenant_id)s", + } + } + if $configure_endpoint_v2 { + keystone_endpoint { "${region}/${auth_name_v2}": + ensure => present, + public_url => "${public_protocol}://${public_address}:${port}/v2/%(tenant_id)s", + admin_url => "${admin_protocol}://${admin_address}:${port}/v2/%(tenant_id)s", + internal_url => "${internal_protocol}://${internal_address}:${port}/v2/%(tenant_id)s", + } + } +} diff --git a/cinder/manifests/logging.pp b/cinder/manifests/logging.pp new file mode 100644 index 000000000..49fb612b4 --- /dev/null +++ b/cinder/manifests/logging.pp @@ -0,0 +1,208 @@ +# Class cinder::logging +# +# cinder extended logging configuration +# +# == parameters +# +# [*logging_context_format_string*] +# (optional) Format string to use for log messages with context. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s\ +# [%(request_id)s %(user_identity)s] %(instance)s%(message)s' +# +# [*logging_default_format_string*] +# (optional) Format string to use for log messages without context. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s\ +# [-] %(instance)s%(message)s' +# +# [*logging_debug_format_suffix*] +# (optional) Formatted data to append to log format when level is DEBUG. +# Defaults to undef. +# Example: '%(funcName)s %(pathname)s:%(lineno)d' +# +# [*logging_exception_prefix*] +# (optional) Prefix each line of exception output with this format. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s' +# +# [*log_config_append*] +# The name of an additional logging configuration file. +# Defaults to undef. +# See https://docs.python.org/2/howto/logging.html +# +# [*default_log_levels*] +# (optional) Hash of logger (keys) and level (values) pairs. +# Defaults to undef. +# Example: +# { 'amqp' => 'WARN', 'amqplib' => 'WARN', 'boto' => 'WARN', +# 'qpid' => 'WARN', 'sqlalchemy' => 'WARN', 'suds' => 'INFO', +# 'iso8601' => 'WARN', +# 'requests.packages.urllib3.connectionpool' => 'WARN' } +# +# [*publish_errors*] +# (optional) Publish error events (boolean value). +# Defaults to undef (false if unconfigured). +# +# [*fatal_deprecations*] +# (optional) Make deprecations fatal (boolean value) +# Defaults to undef (false if unconfigured). +# +# [*instance_format*] +# (optional) If an instance is passed with the log message, format it +# like this (string value). +# Defaults to undef. +# Example: '[instance: %(uuid)s] ' +# +# [*instance_uuid_format*] +# (optional) If an instance UUID is passed with the log message, format +# it like this (string value). +# Defaults to undef. +# Example: instance_uuid_format='[instance: %(uuid)s] ' + +# [*log_date_format*] +# (optional) Format string for %%(asctime)s in log records. +# Defaults to undef. +# Example: 'Y-%m-%d %H:%M:%S' + +class cinder::logging( + $logging_context_format_string = undef, + $logging_default_format_string = undef, + $logging_debug_format_suffix = undef, + $logging_exception_prefix = undef, + $log_config_append = undef, + $default_log_levels = undef, + $publish_errors = undef, + $fatal_deprecations = undef, + $instance_format = undef, + $instance_uuid_format = undef, + $log_date_format = undef, +) { + + if $logging_context_format_string { + cinder_config { + 'DEFAULT/logging_context_format_string' : + value => $logging_context_format_string; + } + } + else { + cinder_config { + 'DEFAULT/logging_context_format_string' : ensure => absent; + } + } + + if $logging_default_format_string { + cinder_config { + 'DEFAULT/logging_default_format_string' : + value => $logging_default_format_string; + } + } + else { + cinder_config { + 'DEFAULT/logging_default_format_string' : ensure => absent; + } + } + + if $logging_debug_format_suffix { + cinder_config { + 'DEFAULT/logging_debug_format_suffix' : + value => $logging_debug_format_suffix; + } + } + else { + cinder_config { + 'DEFAULT/logging_debug_format_suffix' : ensure => absent; + } + } + + if $logging_exception_prefix { + cinder_config { + 'DEFAULT/logging_exception_prefix' : value => $logging_exception_prefix; + } + } + else { + cinder_config { + 'DEFAULT/logging_exception_prefix' : ensure => absent; + } + } + + if $log_config_append { + cinder_config { + 'DEFAULT/log_config_append' : value => $log_config_append; + } + } + else { + cinder_config { + 'DEFAULT/log_config_append' : ensure => absent; + } + } + + if $default_log_levels { + cinder_config { + 'DEFAULT/default_log_levels' : + value => join(sort(join_keys_to_values($default_log_levels, '=')), ','); + } + } + else { + cinder_config { + 'DEFAULT/default_log_levels' : ensure => absent; + } + } + + if $publish_errors { + cinder_config { + 'DEFAULT/publish_errors' : value => $publish_errors; + } + } + else { + cinder_config { + 'DEFAULT/publish_errors' : ensure => absent; + } + } + + if $fatal_deprecations { + cinder_config { + 'DEFAULT/fatal_deprecations' : value => $fatal_deprecations; + } + } + else { + cinder_config { + 'DEFAULT/fatal_deprecations' : ensure => absent; + } + } + + if $instance_format { + cinder_config { + 'DEFAULT/instance_format' : value => $instance_format; + } + } + else { + cinder_config { + 'DEFAULT/instance_format' : ensure => absent; + } + } + + if $instance_uuid_format { + cinder_config { + 'DEFAULT/instance_uuid_format' : value => $instance_uuid_format; + } + } + else { + cinder_config { + 'DEFAULT/instance_uuid_format' : ensure => absent; + } + } + + if $log_date_format { + cinder_config { + 'DEFAULT/log_date_format' : value => $log_date_format; + } + } + else { + cinder_config { + 'DEFAULT/log_date_format' : ensure => absent; + } + } + + +} diff --git a/cinder/manifests/params.pp b/cinder/manifests/params.pp new file mode 100644 index 000000000..23101dc2c --- /dev/null +++ b/cinder/manifests/params.pp @@ -0,0 +1,59 @@ +# +class cinder::params { + + $cinder_conf = '/etc/cinder/cinder.conf' + $cinder_paste_api_ini = '/etc/cinder/api-paste.ini' + + if $::osfamily == 'Debian' { + $package_name = 'cinder-common' + $client_package = 'python-cinderclient' + $api_package = 'cinder-api' + $api_service = 'cinder-api' + $backup_package = 'cinder-backup' + $backup_service = 'cinder-backup' + $scheduler_package = 'cinder-scheduler' + $scheduler_service = 'cinder-scheduler' + $volume_package = 'cinder-volume' + $volume_service = 'cinder-volume' + $db_sync_command = 'cinder-manage db sync' + $tgt_package_name = 'tgt' + $tgt_service_name = 'tgt' + $ceph_init_override = '/etc/init/cinder-volume.override' + $iscsi_helper = 'tgtadm' + $lio_package_name = 'targetcli' + + } elsif($::osfamily == 'RedHat') { + + $package_name = 'openstack-cinder' + $client_package = 'python-cinderclient' + $api_package = false + $api_service = 'openstack-cinder-api' + $backup_package = false + $backup_service = 'openstack-cinder-backup' + $scheduler_package = false + $scheduler_service = 'openstack-cinder-scheduler' + $volume_package = false + $volume_service = 'openstack-cinder-volume' + $db_sync_command = 'cinder-manage db sync' + $tgt_package_name = 'scsi-target-utils' + $tgt_service_name = 'tgtd' + $ceph_init_override = '/etc/sysconfig/openstack-cinder-volume' + $lio_package_name = 'targetcli' + + case $::operatingsystem { + 'RedHat', 'CentOS', 'Scientific': { + if $::operatingsystemmajrelease >= 7 { + $iscsi_helper = 'lioadm' + } else { + $iscsi_helper = 'tgtadm' + } + } + default: { + $iscsi_helper = 'tgtadm' + } + } + + } else { + fail("unsuported osfamily ${::osfamily}, currently Debian and Redhat are the only supported platforms") + } +} diff --git a/cinder/manifests/qpid.pp b/cinder/manifests/qpid.pp new file mode 100644 index 000000000..6b4d29852 --- /dev/null +++ b/cinder/manifests/qpid.pp @@ -0,0 +1,35 @@ +# +# class for installing qpid server for cinder +# +# +class cinder::qpid( + $enabled = true, + $user='guest', + $password='guest', + $file='/var/lib/qpidd/qpidd.sasldb', + $realm='OPENSTACK' +) { + + # only configure cinder after the queue is up + Class['qpid::server'] -> Package<| title == 'cinder' |> + + if ($enabled) { + $service_ensure = 'running' + + qpid_user { $user: + password => $password, + file => $file, + realm => $realm, + provider => 'saslpasswd2', + require => Class['qpid::server'], + } + + } else { + $service_ensure = 'stopped' + } + + class { 'qpid::server': + service_ensure => $service_ensure + } + +} diff --git a/cinder/manifests/quota.pp b/cinder/manifests/quota.pp new file mode 100644 index 000000000..4542d468f --- /dev/null +++ b/cinder/manifests/quota.pp @@ -0,0 +1,34 @@ +# == Class: cinder::quota +# +# Setup and configure Cinder quotas. +# +# === Parameters +# +# [*quota_volumes*] +# (optional) Number of volumes allowed per project. Defaults to 10. +# +# [*quota_snapshots*] +# (optional) Number of volume snapshots allowed per project. Defaults to 10. +# +# [*quota_gigabytes*] +# (optional) Number of volume gigabytes (snapshots are also included) +# allowed per project. Defaults to 1000. +# +# [*quota_driver*] +# (optional) Default driver to use for quota checks. +# Defaults to 'cinder.quota.DbQuotaDriver'. +# +class cinder::quota ( + $quota_volumes = 10, + $quota_snapshots = 10, + $quota_gigabytes = 1000, + $quota_driver = 'cinder.quota.DbQuotaDriver' +) { + + cinder_config { + 'DEFAULT/quota_volumes': value => $quota_volumes; + 'DEFAULT/quota_snapshots': value => $quota_snapshots; + 'DEFAULT/quota_gigabytes': value => $quota_gigabytes; + 'DEFAULT/quota_driver': value => $quota_driver; + } +} diff --git a/cinder/manifests/rabbitmq.pp b/cinder/manifests/rabbitmq.pp new file mode 100644 index 000000000..1c69f3559 --- /dev/null +++ b/cinder/manifests/rabbitmq.pp @@ -0,0 +1,81 @@ +# == Class: cinder::rabbitmq +# +# Installs and manages rabbitmq server for cinder +# +# == Parameters: +# +# [*userid*] +# (optional) The username to use when connecting to Rabbit +# Defaults to 'guest' +# +# [*password*] +# (optional) The password to use when connecting to Rabbit +# Defaults to 'guest' +# +# [*port*] +# (optional) The port to use when connecting to Rabbit +# Defaults to '5672' +# +# [*virtual_host*] +# (optional) The virtual host to use when connecting to Rabbit +# Defaults to '/' +# +# [*enabled*] +# (optional) Whether to enable the Rabbit service +# Defaults to false +# +# [*rabbitmq_class*] +# (optional) The rabbitmq puppet class to depend on, +# which is dependent on the puppet-rabbitmq version. +# Use the default for 1.x, use 'rabbitmq' for 3.x +# Defaults to 'rabbitmq::server' +# +class cinder::rabbitmq( + $userid = 'guest', + $password = 'guest', + $port = '5672', + $virtual_host = '/', + $enabled = true, + $rabbitmq_class = 'rabbitmq::server', +) { + + # only configure cinder after the queue is up + Class[$rabbitmq_class] -> Anchor<| title == 'cinder-start' |> + + if ($enabled) { + if $userid == 'guest' { + $delete_guest_user = false + } else { + $delete_guest_user = true + rabbitmq_user { $userid: + admin => true, + password => $password, + provider => 'rabbitmqctl', + require => Class[$rabbitmq_class], + } + # I need to figure out the appropriate permissions + rabbitmq_user_permissions { "${userid}@${virtual_host}": + configure_permission => '.*', + write_permission => '.*', + read_permission => '.*', + provider => 'rabbitmqctl', + }->Anchor<| title == 'cinder-start' |> + } + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + + class { $rabbitmq_class: + service_ensure => $service_ensure, + port => $port, + delete_guest_user => $delete_guest_user, + } + + if ($enabled) { + rabbitmq_vhost { $virtual_host: + provider => 'rabbitmqctl', + require => Class[$rabbitmq_class], + } + } +} diff --git a/cinder/manifests/scheduler.pp b/cinder/manifests/scheduler.pp new file mode 100644 index 000000000..26b1e5854 --- /dev/null +++ b/cinder/manifests/scheduler.pp @@ -0,0 +1,46 @@ +# +class cinder::scheduler ( + $scheduler_driver = false, + $package_ensure = 'present', + $enabled = true, + $manage_service = true +) { + + include cinder::params + + Cinder_config<||> ~> Service['cinder-scheduler'] + Cinder_api_paste_ini<||> ~> Service['cinder-scheduler'] + Exec<| title == 'cinder-manage db_sync' |> ~> Service['cinder-scheduler'] + + if $scheduler_driver { + cinder_config { + 'DEFAULT/scheduler_driver': value => $scheduler_driver; + } + } + + if $::cinder::params::scheduler_package { + Package['cinder-scheduler'] -> Cinder_config<||> + Package['cinder-scheduler'] -> Cinder_api_paste_ini<||> + Package['cinder-scheduler'] -> Service['cinder-scheduler'] + package { 'cinder-scheduler': + ensure => $package_ensure, + name => $::cinder::params::scheduler_package, + } + } + + if $manage_service { + if $enabled { + $ensure = 'running' + } else { + $ensure = 'stopped' + } + } + + service { 'cinder-scheduler': + ensure => $ensure, + name => $::cinder::params::scheduler_service, + enable => $enabled, + hasstatus => true, + require => Package['cinder'], + } +} diff --git a/cinder/manifests/setup_test_volume.pp b/cinder/manifests/setup_test_volume.pp new file mode 100644 index 000000000..e747e101b --- /dev/null +++ b/cinder/manifests/setup_test_volume.pp @@ -0,0 +1,59 @@ +# == Class: cinder::setup_test_volume +# +# Setup a volume group on a loop device for test purposes. +# +# === Parameters +# +# [*volume_name*] +# Volume group name. Defaults to 'cinder-volumes'. +# +# [*size*] +# Volume group size. Defaults to '4G'. +# +# [*loopback_device*] +# Loop device name. Defaults to '/dev/loop2'. +# +# [*volume_path*] +# Volume image location. Defaults to '/var/lib/cinder'. +class cinder::setup_test_volume( + $volume_name = 'cinder-volumes', + $volume_path = '/var/lib/cinder', + $size = '4G', + $loopback_device = '/dev/loop2' +) { + + package { 'lvm2': + ensure => present, + } ~> + + file { $volume_path: + ensure => directory, + owner => 'cinder', + group => 'cinder', + require => Package['cinder'], + } ~> + + exec { "create_${volume_path}/${volume_name}": + command => "dd if=/dev/zero of=\"${volume_path}/${volume_name}\" bs=1 count=0 seek=${size}", + path => ['/bin','/usr/bin','/sbin','/usr/sbin'], + unless => "stat ${volume_path}/${volume_name}", + } ~> + + exec { "losetup ${loopback_device} ${volume_path}/${volume_name}": + path => ['/bin','/usr/bin','/sbin','/usr/sbin'], + refreshonly => true, + } ~> + + exec { "pvcreate ${loopback_device}": + path => ['/bin','/usr/bin','/sbin','/usr/sbin'], + unless => "pvdisplay | grep ${volume_name}", + refreshonly => true, + } ~> + + exec { "vgcreate ${volume_name} ${loopback_device}": + path => ['/bin','/usr/bin','/sbin','/usr/sbin'], + refreshonly => true, + } + +} + diff --git a/cinder/manifests/type.pp b/cinder/manifests/type.pp new file mode 100644 index 000000000..a0c78bce3 --- /dev/null +++ b/cinder/manifests/type.pp @@ -0,0 +1,81 @@ +# ==Define: cinder::type +# +# Creates cinder type and assigns backends. +# +# === Parameters +# +# [*os_password*] +# (required) The keystone tenant:username password. +# +# [*set_key*] +# (optional) Must be used with set_value. Accepts a single string be used +# as the key in type_set +# +# [*set_value*] +# (optional) Accepts list of strings or singular string. A list of values +# passed to type_set +# +# [*os_tenant_name*] +# (optional) The keystone tenant name. Defaults to 'admin'. +# +# [*os_username*] +# (optional) The keystone user name. Defaults to 'admin. +# +# [*os_auth_url*] +# (optional) The keystone auth url. Defaults to 'http://127.0.0.1:5000/v2.0/'. +# +# [*os_region_name*] +# (optional) The keystone region name. Default is unset. +# +# Author: Andrew Woodward + +define cinder::type ( + $os_password, + $set_key = undef, + $set_value = undef, + $os_tenant_name = 'admin', + $os_username = 'admin', + $os_auth_url = 'http://127.0.0.1:5000/v2.0/', + $os_region_name = undef, + ) { + + $volume_name = $name + +# TODO: (xarses) This should be moved to a ruby provider so that among other +# reasons, the credential discovery magic can occur like in neutron. + + $cinder_env = [ + "OS_TENANT_NAME=${os_tenant_name}", + "OS_USERNAME=${os_username}", + "OS_PASSWORD=${os_password}", + "OS_AUTH_URL=${os_auth_url}", + ] + + if $os_region_name { + $region_env = ["OS_REGION_NAME=${os_region_name}"] + } + else { + $region_env = [] + } + + exec {"cinder type-create ${volume_name}": + command => "cinder type-create ${volume_name}", + unless => "cinder type-list | grep ${volume_name}", + environment => concat($cinder_env, $region_env), + require => Package['python-cinderclient'], + path => ['/usr/bin', '/bin'], + } + + if ($set_value and $set_key) { + Exec["cinder type-create ${volume_name}"] -> + cinder::type_set { $set_value: + type => $volume_name, + key => $set_key, + os_password => $os_password, + os_tenant_name => $os_tenant_name, + os_username => $os_username, + os_auth_url => $os_auth_url, + os_region_name => $os_region_name, + } + } +} diff --git a/cinder/manifests/type_set.pp b/cinder/manifests/type_set.pp new file mode 100644 index 000000000..acbe7fda7 --- /dev/null +++ b/cinder/manifests/type_set.pp @@ -0,0 +1,64 @@ +# ==Define: cinder::type_set +# +# Assigns keys after the volume type is set. +# +# === Parameters +# +# [*os_password*] +# (required) The keystone tenant:username password. +# +# [*type*] +# (required) Accepts single name of type to set. +# +# [*key*] +# (required) the key name that we are setting the value for. +# +# [*os_tenant_name*] +# (optional) The keystone tenant name. Defaults to 'admin'. +# +# [*os_username*] +# (optional) The keystone user name. Defaults to 'admin. +# +# [*os_auth_url*] +# (optional) The keystone auth url. Defaults to 'http://127.0.0.1:5000/v2.0/'. +# +# [*os_region_name*] +# (optional) The keystone region name. Default is unset. +# +# Author: Andrew Woodward + + +define cinder::type_set ( + $type, + $key, + $os_password, + $os_tenant_name = 'admin', + $os_username = 'admin', + $os_auth_url = 'http://127.0.0.1:5000/v2.0/', + $os_region_name = undef, + ) { + +# TODO: (xarses) This should be moved to a ruby provider so that among other +# reasons, the credential discovery magic can occur like in neutron. + + $cinder_env = [ + "OS_TENANT_NAME=${os_tenant_name}", + "OS_USERNAME=${os_username}", + "OS_PASSWORD=${os_password}", + "OS_AUTH_URL=${os_auth_url}", + ] + + if $os_region_name { + $region_env = ["OS_REGION_NAME=${os_region_name}"] + } + else { + $region_env = [] + } + + exec {"cinder type-key ${type} set ${key}=${name}": + path => ['/usr/bin', '/bin'], + command => "cinder type-key ${type} set ${key}=${name}", + environment => concat($cinder_env, $region_env), + require => Package['python-cinderclient'] + } +} diff --git a/cinder/manifests/vmware.pp b/cinder/manifests/vmware.pp new file mode 100644 index 000000000..b254eaf2d --- /dev/null +++ b/cinder/manifests/vmware.pp @@ -0,0 +1,53 @@ +# ==Define: cinder::vmware +# +# Creates vmdk specific disk file type & clone type. +# +# === Parameters +# +# [*os_password*] +# (required) The keystone tenant:username password. +# +# [*os_tenant_name*] +# (optional) The keystone tenant name. Defaults to 'admin'. +# +# [*os_username*] +# (optional) The keystone user name. Defaults to 'admin. +# +# [*os_auth_url*] +# (optional) The keystone auth url. Defaults to 'http://127.0.0.1:5000/v2.0/'. +# +class cinder::vmware ( + $os_password, + $os_tenant_name = 'admin', + $os_username = 'admin', + $os_auth_url = 'http://127.0.0.1:5000/v2.0/' + ) { + + Cinder::Type { + os_password => $os_password, + os_tenant_name => $os_tenant_name, + os_username => $os_username, + os_auth_url => $os_auth_url + } + + cinder::type {'vmware-thin': + set_value => 'thin', + set_key => 'vmware:vmdk_type' + } + cinder::type {'vmware-thick': + set_value => 'thick', + set_key => 'vmware:vmdk_type' + } + cinder::type {'vmware-eagerZeroedThick': + set_value => 'eagerZeroedThick', + set_key => 'vmware:vmdk_type' + } + cinder::type {'vmware-full': + set_value => 'full', + set_key => 'vmware:clone_type' + } + cinder::type {'vmware-linked': + set_value => 'linked', + set_key => 'vmware:clone_type' + } +} \ No newline at end of file diff --git a/cinder/manifests/volume.pp b/cinder/manifests/volume.pp new file mode 100644 index 000000000..6617991eb --- /dev/null +++ b/cinder/manifests/volume.pp @@ -0,0 +1,40 @@ +# $volume_name_template = volume-%s +class cinder::volume ( + $package_ensure = 'present', + $enabled = true, + $manage_service = true +) { + + include cinder::params + + Cinder_config<||> ~> Service['cinder-volume'] + Cinder_api_paste_ini<||> ~> Service['cinder-volume'] + Exec<| title == 'cinder-manage db_sync' |> ~> Service['cinder-volume'] + + if $::cinder::params::volume_package { + Package['cinder-volume'] -> Cinder_config<||> + Package['cinder-volume'] -> Cinder_api_paste_ini<||> + Package['cinder'] -> Package['cinder-volume'] + Package['cinder-volume'] -> Service['cinder-volume'] + package { 'cinder-volume': + ensure => $package_ensure, + name => $::cinder::params::volume_package, + } + } + + if $manage_service { + if $enabled { + $ensure = 'running' + } else { + $ensure = 'stopped' + } + } + + service { 'cinder-volume': + ensure => $ensure, + name => $::cinder::params::volume_service, + enable => $enabled, + hasstatus => true, + require => Package['cinder'], + } +} diff --git a/cinder/manifests/volume/eqlx.pp b/cinder/manifests/volume/eqlx.pp new file mode 100644 index 000000000..7906eefdb --- /dev/null +++ b/cinder/manifests/volume/eqlx.pp @@ -0,0 +1,74 @@ +# == define: cinder::volume::eqlx +# +# Configure the Dell EqualLogic driver for cinder. +# +# === Parameters +# +# [*san_ip*] +# (required) The IP address of the Dell EqualLogic array. +# +# [*san_login*] +# (required) The account to use for issuing SSH commands. +# +# [*san_password*] +# (required) The password for the specified SSH account. +# +# [*san_thin_provision*] +# (optional) Whether or not to use thin provisioning for volumes. +# Defaults to false +# +# [*eqlx_group_name*] +# (optional) The CLI prompt message without '>'. +# Defaults to 'group-0' +# +# [*eqlx_pool*] +# (optional) The pool in which volumes will be created. +# Defaults to 'default' +# +# [*eqlx_use_chap*] +# (optional) Use CHAP authentification for targets? +# Defaults to false +# +# [*eqlx_chap_login*] +# (optional) An existing CHAP account name. +# Defaults to 'chapadmin' +# +# [*eqlx_chap_password*] +# (optional) The password for the specified CHAP account name. +# Defaults to '12345' +# +# [*eqlx_cli_timeout*] +# (optional) The timeout for the Group Manager cli command execution. +# Defaults to 30 seconds +# +# [*eqlx_cli_max_retries*] +# (optional) The maximum retry count for reconnection. +# Defaults to 5 +# +class cinder::volume::eqlx ( + $san_ip, + $san_login, + $san_password, + $san_thin_provision = false, + $eqlx_group_name = 'group-0', + $eqlx_pool = 'default', + $eqlx_use_chap = false, + $eqlx_chap_login = 'chapadmin', + $eqlx_chap_password = '12345', + $eqlx_cli_timeout = 30, + $eqlx_cli_max_retries = 5, +) { + cinder::backend::eqlx { 'DEFAULT': + san_ip => $san_ip, + san_login => $san_login, + san_password => $san_password, + san_thin_provision => $san_thin_provision, + eqlx_group_name => $eqlx_group_name, + eqlx_pool => $eqlx_pool, + eqlx_use_chap => $eqlx_use_chap, + eqlx_chap_login => $eqlx_chap_login, + eqlx_chap_password => $eqlx_chap_password, + eqlx_cli_timeout => $eqlx_cli_timeout, + eqlx_cli_max_retries => $eqlx_cli_max_retries, + } +} diff --git a/cinder/manifests/volume/glusterfs.pp b/cinder/manifests/volume/glusterfs.pp new file mode 100644 index 000000000..f5de750a6 --- /dev/null +++ b/cinder/manifests/volume/glusterfs.pp @@ -0,0 +1,48 @@ +# +# == Class: cinder::volume::glusterfs +# +# Configures Cinder to use GlusterFS as a volume driver +# +# === Parameters +# +# [*glusterfs_shares*] +# (required) An array of GlusterFS volume locations. +# Must be an array even if there is only one volume. +# +# [*glusterfs_disk_util*] +# Removed in Icehouse. +# +# [*glusterfs_sparsed_volumes*] +# (optional) Whether or not to use sparse (thin) volumes. +# Defaults to undef which uses the driver's default of "true". +# +# [*glusterfs_mount_point_base*] +# (optional) Where to mount the Gluster volumes. +# Defaults to undef which uses the driver's default of "$state_path/mnt". +# +# [*glusterfs_shares_config*] +# (optional) The config file to store the given $glusterfs_shares. +# Defaults to '/etc/cinder/shares.conf' +# +# === Examples +# +# class { 'cinder::volume::glusterfs': +# glusterfs_shares = ['192.168.1.1:/volumes'], +# } +# +class cinder::volume::glusterfs ( + $glusterfs_shares, + $glusterfs_disk_util = false, + $glusterfs_sparsed_volumes = undef, + $glusterfs_mount_point_base = undef, + $glusterfs_shares_config = '/etc/cinder/shares.conf' +) { + + cinder::backend::glusterfs { 'DEFAULT': + glusterfs_shares => $glusterfs_shares, + glusterfs_disk_util => $glusterfs_disk_util, + glusterfs_sparsed_volumes => $glusterfs_sparsed_volumes, + glusterfs_mount_point_base => $glusterfs_mount_point_base, + glusterfs_shares_config => $glusterfs_shares_config, + } +} diff --git a/cinder/manifests/volume/iscsi.pp b/cinder/manifests/volume/iscsi.pp new file mode 100644 index 000000000..cb5186221 --- /dev/null +++ b/cinder/manifests/volume/iscsi.pp @@ -0,0 +1,15 @@ +# +class cinder::volume::iscsi ( + $iscsi_ip_address, + $volume_group = 'cinder-volumes', + $iscsi_helper = $::cinder::params::iscsi_helper, +) { + + include cinder::params + + cinder::backend::iscsi { 'DEFAULT': + iscsi_ip_address => $iscsi_ip_address, + volume_group => $volume_group, + iscsi_helper => $iscsi_helper + } +} diff --git a/cinder/manifests/volume/netapp.pp b/cinder/manifests/volume/netapp.pp new file mode 100644 index 000000000..c06fc5550 --- /dev/null +++ b/cinder/manifests/volume/netapp.pp @@ -0,0 +1,196 @@ +# == Class: cinder::volume::netapp +# +# Configures Cinder to use the NetApp unified volume driver +# +# === Parameters +# +# [*netapp_login*] +# (required) Administrative user account name used to access the storage +# system. +# +# [*netapp_password*] +# (required) Password for the administrative user account specified in the +# netapp_login parameter. +# +# [*netapp_server_hostname*] +# (required) The hostname (or IP address) for the storage system. +# +# [*netapp_server_port*] +# (optional) The TCP port to use for communication with ONTAPI on the +# storage system. Traditionally, port 80 is used for HTTP and port 443 is +# used for HTTPS; however, this value should be changed if an alternate +# port has been configured on the storage system. +# Defaults to 80 +# +# [*netapp_size_multiplier*] +# (optional) The quantity to be multiplied by the requested volume size to +# ensure enough space is available on the virtual storage server (Vserver) to +# fulfill the volume creation request. +# Defaults to 1.2 +# +# [*netapp_storage_family*] +# (optional) The storage family type used on the storage system; valid values +# are ontap_7mode for using Data ONTAP operating in 7-Mode or ontap_cluster +# for using clustered Data ONTAP. +# Defaults to ontap_cluster +# +# [*netapp_storage_protocol*] +# (optional) The storage protocol to be used on the data path with the storage +# system; valid values are iscsi or nfs. +# Defaults to nfs +# +# [*netapp_transport_type*] +# (optional) The transport protocol used when communicating with ONTAPI on the +# storage system. Valid values are http or https. +# Defaults to http +# +# [*netapp_vfiler*] +# (optional) The vFiler unit on which provisioning of block storage volumes +# will be done. This parameter is only used by the driver when connecting to +# an instance with a storage family of Data ONTAP operating in 7-Mode and the +# storage protocol selected is iSCSI. Only use this parameter when utilizing +# the MultiStore feature on the NetApp storage system. +# Defaults to '' +# +# [*netapp_volume_list*] +# (optional) This parameter is only utilized when the storage protocol is +# configured to use iSCSI. This parameter is used to restrict provisioning to +# the specified controller volumes. Specify the value of this parameter to be +# a comma separated list of NetApp controller volume names to be used for +# provisioning. +# Defaults to '' +# +# [*netapp_vserver*] +# (optional) This parameter specifies the virtual storage server (Vserver) +# name on the storage cluster on which provisioning of block storage volumes +# should occur. If using the NFS storage protocol, this parameter is mandatory +# for storage service catalog support (utilized by Cinder volume type +# extra_specs support). If this parameter is specified, the exports belonging +# to the Vserver will only be used for provisioning in the future. Block +# storage volumes on exports not belonging to the Vserver specified by +# this parameter will continue to function normally. +# Defaults to '' +# +# [*expiry_thres_minutes*] +# (optional) This parameter specifies the threshold for last access time for +# images in the NFS image cache. When a cache cleaning cycle begins, images +# in the cache that have not been accessed in the last M minutes, where M is +# the value of this parameter, will be deleted from the cache to create free +# space on the NFS share. +# Defaults to 720 +# +# [*thres_avl_size_perc_start*] +# (optional) If the percentage of available space for an NFS share has +# dropped below the value specified by this parameter, the NFS image cache +# will be cleaned. +# Defaults to 20 +# +# [*thres_avl_size_perc_stop*] +# (optional) When the percentage of available space on an NFS share has reached the +# percentage specified by this parameter, the driver will stop clearing files +# from the NFS image cache that have not been accessed in the last M +# 'minutes, where M is the value of the expiry_thres_minutes parameter. +# Defaults to 60 +# +# [*nfs_shares_config*] +# (optional) File with the list of available NFS shares +# Defaults to '' +# +# [*netapp_copyoffload_tool_path*] +# (optional) This option specifies the path of the NetApp Copy Offload tool +# binary. Ensure that the binary has execute permissions set which allow the +# effective user of the cinder-volume process to execute the file. +# Defaults to '' +# +# [*netapp_controller_ips*] +# (optional) This option is only utilized when the storage family is +# configured to eseries. This option is used to restrict provisioning to the +# specified controllers. Specify the value of this option to be a comma +# separated list of controller hostnames or IP addresses to be used for +# provisioning. +# Defaults to '' +# +# [*netapp_sa_password*] +# (optional) Password for the NetApp E-Series storage array. +# Defaults to '' +# +# [*netapp_storage_pools*] +# (optional) This option is used to restrict provisioning to the specified +# storage pools. Only dynamic disk pools are currently supported. Specify the +# value of this option to be a comma separated list of disk pool names to be +# used for provisioning. +# Defaults to '' +# +# [*netapp_webservice_path*] +# (optional) This option is used to specify the path to the E-Series proxy +# application on a proxy server. The value is combined with the value of the +# netapp_transport_type, netapp_server_hostname, and netapp_server_port +# options to create the URL used by the driver to connect to the proxy +# application. +# Defaults to '/devmgr/v2' +# +# === Examples +# +# class { 'cinder::volume::netapp': +# netapp_login => 'clusterAdmin', +# netapp_password => 'password', +# netapp_server_hostname => 'netapp.mycorp.com', +# netapp_server_port => '443', +# netapp_transport_type => 'https', +# netapp_vserver => 'openstack-vserver', +# } +# +# === Authors +# +# Bob Callaway +# +# === Copyright +# +# Copyright 2013 NetApp, Inc. +# +class cinder::volume::netapp ( + $netapp_login, + $netapp_password, + $netapp_server_hostname, + $netapp_server_port = '80', + $netapp_size_multiplier = '1.2', + $netapp_storage_family = 'ontap_cluster', + $netapp_storage_protocol = 'nfs', + $netapp_transport_type = 'http', + $netapp_vfiler = '', + $netapp_volume_list = '', + $netapp_vserver = '', + $expiry_thres_minutes = '720', + $thres_avl_size_perc_start = '20', + $thres_avl_size_perc_stop = '60', + $nfs_shares_config = '', + $netapp_copyoffload_tool_path = '', + $netapp_controller_ips = '', + $netapp_sa_password = '', + $netapp_storage_pools = '', + $netapp_webservice_path = '/devmgr/v2', +) { + + cinder::backend::netapp { 'DEFAULT': + netapp_login => $netapp_login, + netapp_password => $netapp_password, + netapp_server_hostname => $netapp_server_hostname, + netapp_server_port => $netapp_server_port, + netapp_size_multiplier => $netapp_size_multiplier, + netapp_storage_family => $netapp_storage_family, + netapp_storage_protocol => $netapp_storage_protocol, + netapp_transport_type => $netapp_transport_type, + netapp_vfiler => $netapp_vfiler, + netapp_volume_list => $netapp_volume_list, + netapp_vserver => $netapp_vserver, + expiry_thres_minutes => $expiry_thres_minutes, + thres_avl_size_perc_start => $thres_avl_size_perc_start, + thres_avl_size_perc_stop => $thres_avl_size_perc_stop, + nfs_shares_config => $nfs_shares_config, + netapp_copyoffload_tool_path => $netapp_copyoffload_tool_path, + netapp_controller_ips => $netapp_controller_ips, + netapp_sa_password => $netapp_sa_password, + netapp_storage_pools => $netapp_storage_pools, + netapp_webservice_path => $netapp_webservice_path, + } +} diff --git a/cinder/manifests/volume/nexenta.pp b/cinder/manifests/volume/nexenta.pp new file mode 100644 index 000000000..6b653d8b8 --- /dev/null +++ b/cinder/manifests/volume/nexenta.pp @@ -0,0 +1,52 @@ +# == Class: cinder::volume::nexenta +# +# Setups Cinder with Nexenta volume driver. +# +# === Parameters +# +# [*nexenta_user*] +# (required) User name to connect to Nexenta SA. +# +# [*nexenta_password*] +# (required) Password to connect to Nexenta SA. +# +# [*nexenta_host*] +# (required) IP address of Nexenta SA. +# +# [*nexenta_volume*] +# (optional) Pool on SA that will hold all volumes. Defaults to 'cinder'. +# +# [*nexenta_target_prefix*] +# (optional) IQN prefix for iSCSI targets. Defaults to 'iqn:'. +# +# [*nexenta_target_group_prefix*] +# (optional) Prefix for iSCSI target groups on SA. Defaults to 'cinder/'. +# +# [*nexenta_blocksize*] +# (optional) Block size for volumes. Defaults to '8k'. +# +# [*nexenta_sparse*] +# (optional) Flag to create sparse volumes. Defaults to true. +# +class cinder::volume::nexenta ( + $nexenta_user, + $nexenta_password, + $nexenta_host, + $nexenta_volume = 'cinder', + $nexenta_target_prefix = 'iqn:', + $nexenta_target_group_prefix = 'cinder/', + $nexenta_blocksize = '8k', + $nexenta_sparse = true +) { + + cinder::backend::nexenta { 'DEFAULT': + nexenta_user => $nexenta_user, + nexenta_password => $nexenta_password, + nexenta_host => $nexenta_host, + nexenta_volume => $nexenta_volume, + nexenta_target_prefix => $nexenta_target_prefix, + nexenta_target_group_prefix => $nexenta_target_group_prefix, + nexenta_blocksize => $nexenta_blocksize, + nexenta_sparse => $nexenta_sparse, + } +} diff --git a/cinder/manifests/volume/nfs.pp b/cinder/manifests/volume/nfs.pp new file mode 100644 index 000000000..53edc2d57 --- /dev/null +++ b/cinder/manifests/volume/nfs.pp @@ -0,0 +1,23 @@ +# +class cinder::volume::nfs ( + $nfs_servers = [], + $nfs_mount_options = undef, + $nfs_disk_util = undef, + $nfs_sparsed_volumes = undef, + $nfs_mount_point_base = undef, + $nfs_shares_config = '/etc/cinder/shares.conf', + $nfs_used_ratio = '0.95', + $nfs_oversub_ratio = '1.0', +) { + + cinder::backend::nfs { 'DEFAULT': + nfs_servers => $nfs_servers, + nfs_mount_options => $nfs_mount_options, + nfs_disk_util => $nfs_disk_util, + nfs_sparsed_volumes => $nfs_sparsed_volumes, + nfs_mount_point_base => $nfs_mount_point_base, + nfs_shares_config => $nfs_shares_config, + nfs_used_ratio => $nfs_used_ratio, + nfs_oversub_ratio => $nfs_oversub_ratio, + } +} diff --git a/cinder/manifests/volume/rbd.pp b/cinder/manifests/volume/rbd.pp new file mode 100644 index 000000000..2a3e2248f --- /dev/null +++ b/cinder/manifests/volume/rbd.pp @@ -0,0 +1,64 @@ +# == Class: cinder::volume::rbd +# +# Setup Cinder to use the RBD driver. +# +# === Parameters +# +# [*rbd_pool*] +# (required) Specifies the pool name for the block device driver. +# +# [*rbd_user*] +# (required) A required parameter to configure OS init scripts and cephx. +# +# [*rbd_ceph_conf*] +# (optional) Path to the ceph configuration file to use +# Defaults to '/etc/ceph/ceph.conf' +# +# [*rbd_flatten_volume_from_snapshot*] +# (optional) Enable flatten volumes created from snapshots. +# Defaults to false +# +# [*rbd_secret_uuid*] +# (optional) A required parameter to use cephx. +# Defaults to false +# +# [*volume_tmp_dir*] +# (optional) Location to store temporary image files if the volume +# driver does not write them directly to the volume +# Defaults to false +# +# [*rbd_max_clone_depth*] +# (optional) Maximum number of nested clones that can be taken of a +# volume before enforcing a flatten prior to next clone. +# A value of zero disables cloning +# Defaults to '5' +# +# [*glance_api_version*] +# (optional) DEPRECATED: Use cinder::glance Class instead. +# Glance API version. (Defaults to '2') +# Setting this parameter cause a duplicate resource declaration +# with cinder::glance +# +class cinder::volume::rbd ( + $rbd_pool, + $rbd_user, + $rbd_ceph_conf = '/etc/ceph/ceph.conf', + $rbd_flatten_volume_from_snapshot = false, + $rbd_secret_uuid = false, + $volume_tmp_dir = false, + $rbd_max_clone_depth = '5', + # DEPRECATED PARAMETERS + $glance_api_version = undef, +) { + + cinder::backend::rbd { 'DEFAULT': + rbd_pool => $rbd_pool, + rbd_user => $rbd_user, + rbd_ceph_conf => $rbd_ceph_conf, + rbd_flatten_volume_from_snapshot => $rbd_flatten_volume_from_snapshot, + rbd_secret_uuid => $rbd_secret_uuid, + volume_tmp_dir => $volume_tmp_dir, + rbd_max_clone_depth => $rbd_max_clone_depth, + glance_api_version => $glance_api_version, + } +} diff --git a/cinder/manifests/volume/san.pp b/cinder/manifests/volume/san.pp new file mode 100644 index 000000000..a787cd948 --- /dev/null +++ b/cinder/manifests/volume/san.pp @@ -0,0 +1,74 @@ +# == Class: cinder::volume::san +# +# Configures Cinder volume SAN driver. +# Parameters are particular to each volume driver. +# +# === Parameters +# +# [*volume_driver*] +# (required) Setup cinder-volume to use volume driver. +# +# [*san_thin_provision*] +# (optional) Use thin provisioning for SAN volumes? Defaults to true. +# +# [*san_ip*] +# (optional) IP address of SAN controller. +# +# [*san_login*] +# (optional) Username for SAN controller. Defaults to 'admin'. +# +# [*san_password*] +# (optional) Password for SAN controller. +# +# [*san_private_key*] +# (optional) Filename of private key to use for SSH authentication. +# +# [*san_clustername*] +# (optional) Cluster name to use for creating volumes. +# +# [*san_ssh_port*] +# (optional) SSH port to use with SAN. Defaults to 22. +# +# [*san_is_local*] +# (optional) Execute commands locally instead of over SSH +# use if the volume service is running on the SAN device. +# +# [*ssh_conn_timeout*] +# (optional) SSH connection timeout in seconds. Defaults to 30. +# +# [*ssh_min_pool_conn*] +# (optional) Minimum ssh connections in the pool. +# +# [*ssh_min_pool_conn*] +# (optional) Maximum ssh connections in the pool. +# +class cinder::volume::san ( + $volume_driver, + $san_thin_provision = true, + $san_ip = undef, + $san_login = 'admin', + $san_password = undef, + $san_private_key = undef, + $san_clustername = undef, + $san_ssh_port = 22, + $san_is_local = false, + $ssh_conn_timeout = 30, + $ssh_min_pool_conn = 1, + $ssh_max_pool_conn = 5 +) { + + cinder::backend::san { 'DEFAULT': + volume_driver => $volume_driver, + san_thin_provision => $san_thin_provision, + san_ip => $san_ip, + san_login => $san_login, + san_password => $san_password, + san_private_key => $san_private_key, + san_clustername => $san_clustername, + san_ssh_port => $san_ssh_port, + san_is_local => $san_is_local, + ssh_conn_timeout => $ssh_conn_timeout, + ssh_min_pool_conn => $ssh_min_pool_conn, + ssh_max_pool_conn => $ssh_max_pool_conn, + } +} diff --git a/cinder/manifests/volume/solidfire.pp b/cinder/manifests/volume/solidfire.pp new file mode 100644 index 000000000..e461dfbd8 --- /dev/null +++ b/cinder/manifests/volume/solidfire.pp @@ -0,0 +1,58 @@ +# == Class: cinder::volume::solidfire +# +# Configures Cinder volume SolidFire driver. +# Parameters are particular to each volume driver. +# +# === Parameters +# +# [*volume_driver*] +# (optional) Setup cinder-volume to use SolidFire volume driver. +# Defaults to 'cinder.volume.drivers.solidfire.SolidFire' +# +# [*san_ip*] +# (required) IP address of SolidFire clusters MVIP. +# +# [*san_login*] +# (required) Username for SolidFire admin account. +# +# [*san_password*] +# (required) Password for SolidFire admin account. +# +# [*sf_emulate_512*] +# (optional) Use 512 byte emulation for volumes. +# Defaults to True +# +# [*sf_allow_tenant_qos*] +# (optional) Allow tenants to specify QoS via volume metadata. +# Defaults to False +# +# [*sf_account_prefix*] +# (optional) Prefix to use when creating tenant accounts on SolidFire Cluster. +# Defaults to None, so account name is simply the tenant-uuid +# +# [*sf_api_port*] +# (optional) Port ID to use to connect to SolidFire API. +# Defaults to 443 +# +class cinder::volume::solidfire( + $san_ip, + $san_login, + $san_password, + $volume_driver = 'cinder.volume.drivers.solidfire.SolidFire', + $sf_emulate_512 = true, + $sf_allow_tenant_qos = false, + $sf_account_prefix = '', + $sf_api_port = '443' +) { + + cinder::backend::solidfire { 'DEFAULT': + san_ip => $san_ip, + san_login => $san_login, + san_password => $san_password, + volume_driver => $volume_driver, + sf_emulate_512 => $sf_emulate_512, + sf_allow_tenant_qos => $sf_allow_tenant_qos, + sf_account_prefix => $sf_account_prefix, + sf_api_port => $sf_api_port, + } +} diff --git a/cinder/manifests/volume/vmdk.pp b/cinder/manifests/volume/vmdk.pp new file mode 100644 index 000000000..f9a6b10cf --- /dev/null +++ b/cinder/manifests/volume/vmdk.pp @@ -0,0 +1,72 @@ +# == define: cinder::volume::vmdk +# +# Configure the VMware VMDK driver for cinder. +# +# === Parameters +# +# [*host_ip*] +# The IP address of the VMware vCenter server. +# +# [*host_username*] +# The username for connection to VMware vCenter server. +# +# [*host_password*] +# The password for connection to VMware vCenter server. +# +# [*api_retry_count*] +# (optional) The number of times we retry on failures, +# e.g., socket error, etc. +# Defaults to 10. +# +# [*max_object_retrieval*] +# (optional) The maximum number of ObjectContent data objects that should +# be returned in a single result. A positive value will cause +# the operation to suspend the retrieval when the count of +# objects reaches the specified maximum. The server may still +# limit the count to something less than the configured value. +# Any remaining objects may be retrieved with additional requests. +# Defaults to 100. +# +# [*task_poll_interval*] +# (optional) The interval in seconds used for polling of remote tasks. +# Defaults to 5. +# +# [*image_transfer_timeout_secs*] +# (optional) The timeout in seconds for VMDK volume transfer between Cinder and Glance. +# Defaults to 7200. +# +# [*wsdl_location*] +# (optional) VIM Service WSDL Location e.g +# http:///vimService.wsdl. Optional over-ride to +# default location for bug work-arounds. +# Defaults to None. +# +# [*volume_folder*] +# (optional) The name for the folder in the VC datacenter that will contain cinder volumes. +# Defaults to 'cinder-volumes'. +# + +class cinder::volume::vmdk( + $host_ip, + $host_username, + $host_password, + $volume_folder = 'cinder-volumes', + $api_retry_count = 10, + $max_object_retrieval = 100, + $task_poll_interval = 5, + $image_transfer_timeout_secs = 7200, + $wsdl_location = undef +) { + + cinder::backend::vmdk { 'DEFAULT': + host_ip => $host_ip, + host_username => $host_username, + host_password => $host_password, + volume_folder => $volume_folder, + api_retry_count => $api_retry_count, + max_object_retrieval => $max_object_retrieval, + task_poll_interval => $task_poll_interval, + image_transfer_timeout_secs => $image_transfer_timeout_secs, + wsdl_location => $wsdl_location, + } +} diff --git a/cinder/spec/classes/cinder_api_spec.rb b/cinder/spec/classes/cinder_api_spec.rb new file mode 100644 index 000000000..93cb782ef --- /dev/null +++ b/cinder/spec/classes/cinder_api_spec.rb @@ -0,0 +1,187 @@ +require 'spec_helper' + +describe 'cinder::api' do + + let :req_params do + {:keystone_password => 'foo'} + end + let :facts do + {:osfamily => 'Debian', + :processorcount => 8 } + end + + describe 'with only required params' do + let :params do + req_params + end + + it { should contain_service('cinder-api').with( + 'hasstatus' => true, + 'ensure' => 'running' + )} + + it 'should configure cinder api correctly' do + should contain_cinder_config('DEFAULT/auth_strategy').with( + :value => 'keystone' + ) + should contain_cinder_config('DEFAULT/osapi_volume_listen').with( + :value => '0.0.0.0' + ) + should contain_cinder_config('DEFAULT/osapi_volume_workers').with( + :value => '8' + ) + should contain_cinder_config('DEFAULT/default_volume_type').with( + :ensure => 'absent' + ) + should contain_cinder_api_paste_ini('filter:authtoken/service_protocol').with( + :value => 'http' + ) + should contain_cinder_api_paste_ini('filter:authtoken/service_host').with( + :value => 'localhost' + ) + should contain_cinder_api_paste_ini('filter:authtoken/service_port').with( + :value => '5000' + ) + should contain_cinder_api_paste_ini('filter:authtoken/auth_protocol').with( + :value => 'http' + ) + should contain_cinder_api_paste_ini('filter:authtoken/auth_host').with( + :value => 'localhost' + ) + should contain_cinder_api_paste_ini('filter:authtoken/auth_port').with( + :value => '35357' + ) + should contain_cinder_api_paste_ini('filter:authtoken/auth_admin_prefix').with( + :ensure => 'absent' + ) + should contain_cinder_api_paste_ini('filter:authtoken/admin_tenant_name').with( + :value => 'services' + ) + should contain_cinder_api_paste_ini('filter:authtoken/admin_user').with( + :value => 'cinder' + ) + should contain_cinder_api_paste_ini('filter:authtoken/admin_password').with( + :value => 'foo', + :secret => true + ) + + should contain_cinder_api_paste_ini('filter:authtoken/auth_uri').with( + :value => 'http://localhost:5000/' + ) + + should_not contain_cinder_config('DEFAULT/os_region_name') + end + end + + describe 'with a custom region for nova' do + let :params do + req_params.merge({'os_region_name' => 'MyRegion'}) + end + it 'should configure the region for nova' do + should contain_cinder_config('DEFAULT/os_region_name').with( + :value => 'MyRegion' + ) + end + end + + describe 'with a default volume type' do + let :params do + req_params.merge({'default_volume_type' => 'foo'}) + end + it 'should configure the default volume type for cinder' do + should contain_cinder_config('DEFAULT/default_volume_type').with( + :value => 'foo' + ) + end + end + + describe 'with custom auth_uri' do + let :params do + req_params.merge({'keystone_auth_uri' => 'http://foo.bar:8080/v2.0/'}) + end + it 'should configure cinder auth_uri correctly' do + should contain_cinder_api_paste_ini('filter:authtoken/auth_uri').with( + :value => 'http://foo.bar:8080/v2.0/' + ) + end + end + + describe 'with only required params' do + let :params do + req_params.merge({'bind_host' => '192.168.1.3'}) + end + it 'should configure cinder api correctly' do + should contain_cinder_config('DEFAULT/osapi_volume_listen').with( + :value => '192.168.1.3' + ) + end + end + + [ '/keystone', '/keystone/admin', '' ].each do |keystone_auth_admin_prefix| + describe "with keystone_auth_admin_prefix containing incorrect value #{keystone_auth_admin_prefix}" do + let :params do + { + :keystone_auth_admin_prefix => keystone_auth_admin_prefix, + :keystone_password => 'dummy' + } + end + + it { should contain_cinder_api_paste_ini('filter:authtoken/auth_admin_prefix').with( + :value => keystone_auth_admin_prefix + )} + end + end + + [ + '/keystone/', + 'keystone/', + 'keystone', + '/keystone/admin/', + 'keystone/admin/', + 'keystone/admin' + ].each do |keystone_auth_admin_prefix| + describe "with keystone_auth_admin_prefix containing incorrect value #{keystone_auth_admin_prefix}" do + let :params do + { + :keystone_auth_admin_prefix => keystone_auth_admin_prefix, + :keystone_password => 'dummy' + } + end + + it { expect { should contain_cinder_api_paste_ini('filter:authtoken/auth_admin_prefix') }.to \ + raise_error(Puppet::Error, /validate_re\(\): "#{keystone_auth_admin_prefix}" does not match/) } + end + end + + describe 'with enabled false' do + let :params do + req_params.merge({'enabled' => false}) + end + it 'should stop the service' do + should contain_service('cinder-api').with_ensure('stopped') + end + it 'should contain db_sync exec' do + should_not contain_exec('cinder-manage db_sync') + end + end + + describe 'with manage_service false' do + let :params do + req_params.merge({'manage_service' => false}) + end + it 'should not change the state of the service' do + should contain_service('cinder-api').without_ensure + end + end + + describe 'with ratelimits' do + let :params do + req_params.merge({ :ratelimits => '(GET, "*", .*, 100, MINUTE);(POST, "*", .*, 200, MINUTE)' }) + end + + it { should contain_cinder_api_paste_ini('filter:ratelimit/limits').with( + :value => '(GET, "*", .*, 100, MINUTE);(POST, "*", .*, 200, MINUTE)' + )} + end + +end diff --git a/cinder/spec/classes/cinder_backends_spec.rb b/cinder/spec/classes/cinder_backends_spec.rb new file mode 100644 index 000000000..de990d028 --- /dev/null +++ b/cinder/spec/classes/cinder_backends_spec.rb @@ -0,0 +1,83 @@ +# +# Copyright (C) 2014 eNovance SAS +# +# Author: Emilien Macchi +# +# 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. +# +# Unit tests for cinder::backends class +# + +require 'spec_helper' + +describe 'cinder::backends' do + + let :default_params do + {} + end + + let :params do + {} + end + + shared_examples_for 'cinder backends' do + + let :p do + default_params.merge(params) + end + + context 'configure cinder with default parameters' do + before :each do + params.merge!( + :enabled_backends => ['lowcost', 'regular', 'premium'], + :default_volume_type => false + ) + end + + it 'configures cinder.conf with default params' do + should contain_cinder_config('DEFAULT/enabled_backends').with_value(p[:enabled_backends].join(',')) + end + end + + context 'configure cinder with a default volume type' do + before :each do + params.merge!( + :enabled_backends => ['foo', 'bar'], + :default_volume_type => 'regular' + ) + end + + it 'should fail to configure default volume type' do + expect { subject }.to raise_error(Puppet::Error, /The default_volume_type parameter is deprecated in this class, you should declare it in cinder::api./) + end + end + + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'cinder backends' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'cinder backends' + end + +end diff --git a/cinder/spec/classes/cinder_backup_ceph_spec.rb b/cinder/spec/classes/cinder_backup_ceph_spec.rb new file mode 100644 index 000000000..bedd7cdc5 --- /dev/null +++ b/cinder/spec/classes/cinder_backup_ceph_spec.rb @@ -0,0 +1,89 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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. +# +# Unit tests for cinder::ceph class +# + +require 'spec_helper' + +describe 'cinder::backup::ceph' do + + let :default_params do + { :backup_ceph_conf => '/etc/ceph/ceph.conf', + :backup_ceph_user => 'cinder', + :backup_ceph_chunk_size => '134217728', + :backup_ceph_pool => 'backups', + :backup_ceph_stripe_unit => '0', + :backup_ceph_stripe_count => '0' } + end + + let :params do + {} + end + + shared_examples_for 'cinder backup with ceph' do + let :p do + default_params.merge(params) + end + + it 'configures cinder.conf' do + should contain_cinder_config('DEFAULT/backup_driver').with_value('cinder.backup.driver.ceph') + should contain_cinder_config('DEFAULT/backup_ceph_conf').with_value(p[:backup_ceph_conf]) + should contain_cinder_config('DEFAULT/backup_ceph_user').with_value(p[:backup_ceph_user]) + should contain_cinder_config('DEFAULT/backup_ceph_chunk_size').with_value(p[:backup_ceph_chunk_size]) + should contain_cinder_config('DEFAULT/backup_ceph_pool').with_value(p[:backup_ceph_pool]) + should contain_cinder_config('DEFAULT/backup_ceph_stripe_unit').with_value(p[:backup_ceph_stripe_unit]) + should contain_cinder_config('DEFAULT/backup_ceph_stripe_count').with_value(p[:backup_ceph_stripe_count]) + end + + context 'when overriding default parameters' do + before :each do + params.merge!(:backup_ceph_conf => '/tmp/ceph.conf') + params.merge!(:backup_ceph_user => 'toto') + params.merge!(:backup_ceph_chunk_size => '123') + params.merge!(:backup_ceph_pool => 'foo') + params.merge!(:backup_ceph_stripe_unit => '56') + params.merge!(:backup_ceph_stripe_count => '67') + end + it 'should replace default parameters with new values' do + should contain_cinder_config('DEFAULT/backup_ceph_conf').with_value(p[:backup_ceph_conf]) + should contain_cinder_config('DEFAULT/backup_ceph_user').with_value(p[:backup_ceph_user]) + should contain_cinder_config('DEFAULT/backup_ceph_chunk_size').with_value(p[:backup_ceph_chunk_size]) + should contain_cinder_config('DEFAULT/backup_ceph_pool').with_value(p[:backup_ceph_pool]) + should contain_cinder_config('DEFAULT/backup_ceph_stripe_unit').with_value(p[:backup_ceph_stripe_unit]) + should contain_cinder_config('DEFAULT/backup_ceph_stripe_count').with_value(p[:backup_ceph_stripe_count]) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'cinder backup with ceph' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'cinder backup with ceph' + end + +end diff --git a/cinder/spec/classes/cinder_backup_spec.rb b/cinder/spec/classes/cinder_backup_spec.rb new file mode 100644 index 000000000..0f632ccb3 --- /dev/null +++ b/cinder/spec/classes/cinder_backup_spec.rb @@ -0,0 +1,101 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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. +# +# Unit tests for cinder::backup class +# + +require 'spec_helper' + +describe 'cinder::backup' do + + let :default_params do + { :enable => true, + :backup_topic => 'cinder-backup', + :backup_manager => 'cinder.backup.manager.BackupManager', + :backup_api_class => 'cinder.backup.api.API', + :backup_name_template => 'backup-%s' } + end + + let :params do + {} + end + + shared_examples_for 'cinder backup' do + let :p do + default_params.merge(params) + end + + it { should contain_class('cinder::params') } + + it 'installs cinder backup package' do + if platform_params.has_key?(:backup_package) + should contain_package('cinder-backup').with( + :name => platform_params[:backup_package], + :ensure => 'present' + ) + should contain_package('cinder-backup').with_before(/Cinder_config\[.+\]/) + should contain_package('cinder-backup').with_before(/Service\[cinder-backup\]/) + end + end + + it 'ensure cinder backup service is running' do + should contain_service('cinder-backup').with('hasstatus' => true) + end + + it 'configures cinder.conf' do + should contain_cinder_config('DEFAULT/backup_topic').with_value(p[:backup_topic]) + should contain_cinder_config('DEFAULT/backup_manager').with_value(p[:backup_manager]) + should contain_cinder_config('DEFAULT/backup_api_class').with_value(p[:backup_api_class]) + should contain_cinder_config('DEFAULT/backup_name_template').with_value(p[:backup_name_template]) + end + + context 'when overriding backup_name_template' do + before :each do + params.merge!(:backup_name_template => 'foo-bar-%s') + end + it 'should replace default parameter with new value' do + should contain_cinder_config('DEFAULT/backup_name_template').with_value(p[:backup_name_template]) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :backup_package => 'cinder-backup', + :backup_service => 'cinder-backup' } + end + + it_configures 'cinder backup' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :backup_service => 'cinder-backup' } + end + + it_configures 'cinder backup' + end + +end diff --git a/cinder/spec/classes/cinder_backup_swift_spec.rb b/cinder/spec/classes/cinder_backup_swift_spec.rb new file mode 100644 index 000000000..dae1bd61d --- /dev/null +++ b/cinder/spec/classes/cinder_backup_swift_spec.rb @@ -0,0 +1,85 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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. +# +# Unit tests for cinder::backup::swift class +# + +require 'spec_helper' + +describe 'cinder::backup::swift' do + + let :default_params do + { :backup_swift_url => 'http://localhost:8080/v1/AUTH_', + :backup_swift_container => 'volumes_backup', + :backup_swift_object_size => '52428800', + :backup_swift_retry_attempts => '3', + :backup_swift_retry_backoff => '2' } + end + + let :params do + {} + end + + shared_examples_for 'cinder backup with swift' do + let :p do + default_params.merge(params) + end + + it 'configures cinder.conf' do + should contain_cinder_config('DEFAULT/backup_driver').with_value('cinder.backup.drivers.swift') + should contain_cinder_config('DEFAULT/backup_swift_url').with_value(p[:backup_swift_url]) + should contain_cinder_config('DEFAULT/backup_swift_container').with_value(p[:backup_swift_container]) + should contain_cinder_config('DEFAULT/backup_swift_object_size').with_value(p[:backup_swift_object_size]) + should contain_cinder_config('DEFAULT/backup_swift_retry_attempts').with_value(p[:backup_swift_retry_attempts]) + should contain_cinder_config('DEFAULT/backup_swift_retry_backoff').with_value(p[:backup_swift_retry_backoff]) + end + + context 'when overriding default parameters' do + before :each do + params.merge!(:backup_swift_url => 'https://controller2:8080/v1/AUTH_') + params.merge!(:backup_swift_container => 'toto') + params.merge!(:backup_swift_object_size => '123') + params.merge!(:backup_swift_retry_attempts => '99') + params.merge!(:backup_swift_retry_backoff => '56') + end + it 'should replace default parameters with new values' do + should contain_cinder_config('DEFAULT/backup_swift_url').with_value(p[:backup_swift_url]) + should contain_cinder_config('DEFAULT/backup_swift_container').with_value(p[:backup_swift_container]) + should contain_cinder_config('DEFAULT/backup_swift_object_size').with_value(p[:backup_swift_object_size]) + should contain_cinder_config('DEFAULT/backup_swift_retry_attempts').with_value(p[:backup_swift_retry_attempts]) + should contain_cinder_config('DEFAULT/backup_swift_retry_backoff').with_value(p[:backup_swift_retry_backoff]) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'cinder backup with swift' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'cinder backup with swift' + end + +end diff --git a/cinder/spec/classes/cinder_ceilometer_spec.rb b/cinder/spec/classes/cinder_ceilometer_spec.rb new file mode 100644 index 000000000..e1b52a10d --- /dev/null +++ b/cinder/spec/classes/cinder_ceilometer_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe 'cinder::ceilometer' do + + describe 'with default parameters' do + it 'contains default values' do + should contain_cinder_config('DEFAULT/notification_driver').with( + :value => 'cinder.openstack.common.notifier.rpc_notifier') + end + end +end diff --git a/cinder/spec/classes/cinder_client_spec.rb b/cinder/spec/classes/cinder_client_spec.rb new file mode 100644 index 000000000..77b51ced8 --- /dev/null +++ b/cinder/spec/classes/cinder_client_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe 'cinder::client' do + it { should contain_package('python-cinderclient').with_ensure('present') } + let :facts do + {:osfamily => 'Debian'} + end + context 'with params' do + let :params do + {:package_ensure => 'latest'} + end + it { should contain_package('python-cinderclient').with_ensure('latest') } + end +end diff --git a/cinder/spec/classes/cinder_db_mysql_spec.rb b/cinder/spec/classes/cinder_db_mysql_spec.rb new file mode 100644 index 000000000..b5faa1db1 --- /dev/null +++ b/cinder/spec/classes/cinder_db_mysql_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe 'cinder::db::mysql' do + + let :req_params do + {:password => 'pw', + } + end + + let :facts do + {:osfamily => 'Debian'} + end + + let :pre_condition do + 'include mysql::server' + end + + describe 'with only required params' do + let :params do + req_params + end + it { should contain_openstacklib__db__mysql('cinder').with( + :user => 'cinder', + :password_hash => '*D821809F681A40A6E379B50D0463EFAE20BDD122', + :host => '127.0.0.1', + :charset => 'utf8' + ) } + end + describe "overriding allowed_hosts param to array" do + let :params do + { + :password => 'cinderpass', + :allowed_hosts => ['127.0.0.1','%'] + } + end + + end + describe "overriding allowed_hosts param to string" do + let :params do + { + :password => 'cinderpass2', + :allowed_hosts => '192.168.1.1' + } + end + + end + + describe "overriding allowed_hosts param equals to host param " do + let :params do + { + :password => 'cinderpass2', + :allowed_hosts => '127.0.0.1' + } + end + + end +end diff --git a/cinder/spec/classes/cinder_db_postgresql_spec.rb b/cinder/spec/classes/cinder_db_postgresql_spec.rb new file mode 100644 index 000000000..93296ae1d --- /dev/null +++ b/cinder/spec/classes/cinder_db_postgresql_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'cinder::db::postgresql' do + + let :req_params do + {:password => 'pw'} + end + + let :facts do + { + :postgres_default_version => '8.4', + :osfamily => 'RedHat', + } + end + + describe 'with only required params' do + let :params do + req_params + end + it { should contain_postgresql__db('cinder').with( + :user => 'cinder', + :password => 'pw' + ) } + end + +end diff --git a/cinder/spec/classes/cinder_db_sync_spec.rb b/cinder/spec/classes/cinder_db_sync_spec.rb new file mode 100644 index 000000000..906d4391a --- /dev/null +++ b/cinder/spec/classes/cinder_db_sync_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'cinder::db::sync' do + + let :facts do + {:osfamily => 'Debian'} + end + it { should contain_exec('cinder-manage db_sync').with( + :command => 'cinder-manage db sync', + :path => '/usr/bin', + :user => 'cinder', + :refreshonly => true, + :logoutput => 'on_failure' + ) } + +end diff --git a/cinder/spec/classes/cinder_glance_spec.rb b/cinder/spec/classes/cinder_glance_spec.rb new file mode 100644 index 000000000..64bf89a52 --- /dev/null +++ b/cinder/spec/classes/cinder_glance_spec.rb @@ -0,0 +1,82 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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. +# +# Unit tests for cinder::glance class +# + +require 'spec_helper' + +describe 'cinder::glance' do + + let :default_params do + { :glance_api_version => '2', + :glance_num_retries => '0', + :glance_api_insecure => false, + :glance_api_ssl_compression => false } + end + + let :params do + {} + end + + shared_examples_for 'cinder with glance' do + let :p do + default_params.merge(params) + end + + it 'configures cinder.conf with default params' do + should contain_cinder_config('DEFAULT/glance_api_version').with_value(p[:glance_api_version]) + should contain_cinder_config('DEFAULT/glance_num_retries').with_value(p[:glance_num_retries]) + should contain_cinder_config('DEFAULT/glance_api_insecure').with_value(p[:glance_api_insecure]) + end + + context 'configure cinder with one glance server' do + before :each do + params.merge!(:glance_api_servers => '10.0.0.1:9292') + end + it 'should configure one glance server' do + should contain_cinder_config('DEFAULT/glance_api_servers').with_value(p[:glance_api_servers]) + end + end + + context 'configure cinder with two glance servers' do + before :each do + params.merge!(:glance_api_servers => ['10.0.0.1:9292','10.0.0.2:9292']) + end + it 'should configure two glance servers' do + should contain_cinder_config('DEFAULT/glance_api_servers').with_value(p[:glance_api_servers].join(',')) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'cinder with glance' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'cinder with glance' + end + +end diff --git a/cinder/spec/classes/cinder_keystone_auth_spec.rb b/cinder/spec/classes/cinder_keystone_auth_spec.rb new file mode 100644 index 000000000..9c3690b7f --- /dev/null +++ b/cinder/spec/classes/cinder_keystone_auth_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe 'cinder::keystone::auth' do + + let :req_params do + {:password => 'pw'} + end + + describe 'with only required params' do + + let :params do + req_params + end + + it 'should contain auth info' do + + should contain_keystone_user('cinder').with( + :ensure => 'present', + :password => 'pw', + :email => 'cinder@localhost', + :tenant => 'services' + ) + should contain_keystone_user_role('cinder@services').with( + :ensure => 'present', + :roles => 'admin' + ) + should contain_keystone_service('cinder').with( + :ensure => 'present', + :type => 'volume', + :description => 'Cinder Service' + ) + should contain_keystone_service('cinderv2').with( + :ensure => 'present', + :type => 'volumev2', + :description => 'Cinder Service v2' + ) + + end + it { should contain_keystone_endpoint('RegionOne/cinder').with( + :ensure => 'present', + :public_url => 'http://127.0.0.1:8776/v1/%(tenant_id)s', + :admin_url => 'http://127.0.0.1:8776/v1/%(tenant_id)s', + :internal_url => 'http://127.0.0.1:8776/v1/%(tenant_id)s' + ) } + it { should contain_keystone_endpoint('RegionOne/cinderv2').with( + :ensure => 'present', + :public_url => 'http://127.0.0.1:8776/v2/%(tenant_id)s', + :admin_url => 'http://127.0.0.1:8776/v2/%(tenant_id)s', + :internal_url => 'http://127.0.0.1:8776/v2/%(tenant_id)s' + ) } + + end + + context 'when overriding endpoint params' do + let :params do + req_params.merge( + :public_address => '10.0.42.1', + :admin_address => '10.0.42.2', + :internal_address => '10.0.42.3', + :region => 'RegionThree', + :port => '4242', + :admin_protocol => 'https', + :internal_protocol => 'https', + :public_protocol => 'https', + :volume_version => 'v42' + ) + end + + it { should contain_keystone_endpoint('RegionThree/cinder').with( + :ensure => 'present', + :public_url => 'https://10.0.42.1:4242/v42/%(tenant_id)s', + :admin_url => 'https://10.0.42.2:4242/v42/%(tenant_id)s', + :internal_url => 'https://10.0.42.3:4242/v42/%(tenant_id)s' + )} + + it { should contain_keystone_endpoint('RegionThree/cinderv2').with( + :ensure => 'present', + :public_url => 'https://10.0.42.1:4242/v2/%(tenant_id)s', + :admin_url => 'https://10.0.42.2:4242/v2/%(tenant_id)s', + :internal_url => 'https://10.0.42.3:4242/v2/%(tenant_id)s' + )} + end + + + describe 'when endpoint should not be configured' do + let :params do + req_params.merge( + :configure_endpoint => false, + :configure_endpoint_v2 => false + ) + end + it { should_not contain_keystone_endpoint('RegionOne/cinder') } + it { should_not contain_keystone_endpoint('RegionOne/cinderv2') } + end + + describe 'when user should not be configured' do + let :params do + req_params.merge( + :configure_user => false + ) + end + + it { should_not contain_keystone_user('cinder') } + + it { should contain_keystone_user_role('cinder@services') } + + it { should contain_keystone_service('cinder').with( + :ensure => 'present', + :type => 'volume', + :description => 'Cinder Service' + ) } + + end + + describe 'when user and user role should not be configured' do + let :params do + req_params.merge( + :configure_user => false, + :configure_user_role => false + ) + end + + it { should_not contain_keystone_user('cinder') } + + it { should_not contain_keystone_user_role('cinder@services') } + + it { should contain_keystone_service('cinder').with( + :ensure => 'present', + :type => 'volume', + :description => 'Cinder Service' + ) } + + end + +end diff --git a/cinder/spec/classes/cinder_logging_spec.rb b/cinder/spec/classes/cinder_logging_spec.rb new file mode 100644 index 000000000..ca967e7c4 --- /dev/null +++ b/cinder/spec/classes/cinder_logging_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe 'cinder::logging' do + + let :params do + { + } + end + + let :log_params do + { + :logging_context_format_string => '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s', + :logging_default_format_string => '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s', + :logging_debug_format_suffix => '%(funcName)s %(pathname)s:%(lineno)d', + :logging_exception_prefix => '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s', + :log_config_append => '/etc/cinder/logging.conf', + :publish_errors => true, + :default_log_levels => { + 'amqp' => 'WARN', 'amqplib' => 'WARN', 'boto' => 'WARN', + 'qpid' => 'WARN', 'sqlalchemy' => 'WARN', 'suds' => 'INFO', + 'iso8601' => 'WARN', + 'requests.packages.urllib3.connectionpool' => 'WARN' }, + :fatal_deprecations => true, + :instance_format => '[instance: %(uuid)s] ', + :instance_uuid_format => '[instance: %(uuid)s] ', + :log_date_format => '%Y-%m-%d %H:%M:%S', + } + end + + shared_examples_for 'cinder-logging' do + + context 'with extended logging options' do + before { params.merge!( log_params ) } + it_configures 'logging params set' + end + + context 'without extended logging options' do + it_configures 'logging params unset' + end + + end + + shared_examples_for 'logging params set' do + it 'enables logging params' do + should contain_cinder_config('DEFAULT/logging_context_format_string').with_value( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s') + + should contain_cinder_config('DEFAULT/logging_default_format_string').with_value( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s') + + should contain_cinder_config('DEFAULT/logging_debug_format_suffix').with_value( + '%(funcName)s %(pathname)s:%(lineno)d') + + should contain_cinder_config('DEFAULT/logging_exception_prefix').with_value( + '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s') + + should contain_cinder_config('DEFAULT/log_config_append').with_value( + '/etc/cinder/logging.conf') + should contain_cinder_config('DEFAULT/publish_errors').with_value( + true) + + should contain_cinder_config('DEFAULT/default_log_levels').with_value( + 'amqp=WARN,amqplib=WARN,boto=WARN,iso8601=WARN,qpid=WARN,requests.packages.urllib3.connectionpool=WARN,sqlalchemy=WARN,suds=INFO') + + should contain_cinder_config('DEFAULT/fatal_deprecations').with_value( + true) + + should contain_cinder_config('DEFAULT/instance_format').with_value( + '[instance: %(uuid)s] ') + + should contain_cinder_config('DEFAULT/instance_uuid_format').with_value( + '[instance: %(uuid)s] ') + + should contain_cinder_config('DEFAULT/log_date_format').with_value( + '%Y-%m-%d %H:%M:%S') + end + end + + + shared_examples_for 'logging params unset' do + [ :logging_context_format_string, :logging_default_format_string, + :logging_debug_format_suffix, :logging_exception_prefix, + :log_config_append, :publish_errors, + :default_log_levels, :fatal_deprecations, + :instance_format, :instance_uuid_format, + :log_date_format, ].each { |param| + it { should contain_cinder_config("DEFAULT/#{param}").with_ensure('absent') } + } + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'cinder-logging' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'cinder-logging' + end + +end diff --git a/cinder/spec/classes/cinder_params_spec.rb b/cinder/spec/classes/cinder_params_spec.rb new file mode 100644 index 000000000..306ce1811 --- /dev/null +++ b/cinder/spec/classes/cinder_params_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe 'cinder::params' do + + let :facts do + {:osfamily => 'Debian'} + end + it 'should compile' do + subject + end + +end diff --git a/cinder/spec/classes/cinder_qpid_spec.rb b/cinder/spec/classes/cinder_qpid_spec.rb new file mode 100644 index 000000000..0ace32090 --- /dev/null +++ b/cinder/spec/classes/cinder_qpid_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe 'cinder::qpid' do + + let :facts do + {:puppetversion => '2.7', + :osfamily => 'RedHat'} + end + + describe 'with defaults' do + + it 'should contain all of the default resources' do + + should contain_class('qpid::server').with( + :service_ensure => 'running', + :port => '5672' + ) + + end + + it 'should contain user' do + + should contain_qpid_user('guest').with( + :password => 'guest', + :file => '/var/lib/qpidd/qpidd.sasldb', + :realm => 'OPENSTACK', + :provider => 'saslpasswd2' + ) + + end + + end + + describe 'when disabled' do + let :params do + { + :enabled => false + } + end + + it 'should be disabled' do + + should_not contain_qpid_user('guest') + should contain_class('qpid::server').with( + :service_ensure => 'stopped' + ) + + end + end + +end diff --git a/cinder/spec/classes/cinder_quota_spec.rb b/cinder/spec/classes/cinder_quota_spec.rb new file mode 100644 index 000000000..ecbc2d97b --- /dev/null +++ b/cinder/spec/classes/cinder_quota_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'cinder::quota' do + + describe 'with default parameters' do + it 'contains default values' do + should contain_cinder_config('DEFAULT/quota_volumes').with( + :value => 10) + should contain_cinder_config('DEFAULT/quota_snapshots').with( + :value => 10) + should contain_cinder_config('DEFAULT/quota_gigabytes').with( + :value => 1000) + should contain_cinder_config('DEFAULT/quota_driver').with( + :value => 'cinder.quota.DbQuotaDriver') + end + end + + describe 'with overridden parameters' do + let :params do + { :quota_volumes => 1000, + :quota_snapshots => 1000, + :quota_gigabytes => 100000 } + end + it 'contains overrided values' do + should contain_cinder_config('DEFAULT/quota_volumes').with( + :value => 1000) + should contain_cinder_config('DEFAULT/quota_snapshots').with( + :value => 1000) + should contain_cinder_config('DEFAULT/quota_gigabytes').with( + :value => 100000) + should contain_cinder_config('DEFAULT/quota_driver').with( + :value => 'cinder.quota.DbQuotaDriver') + end + end +end diff --git a/cinder/spec/classes/cinder_rabbitmq_spec.rb b/cinder/spec/classes/cinder_rabbitmq_spec.rb new file mode 100644 index 000000000..3fcfd7a0a --- /dev/null +++ b/cinder/spec/classes/cinder_rabbitmq_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe 'cinder::rabbitmq' do + + let :facts do + { :puppetversion => '2.7', + :osfamily => 'Debian', + } + end + + describe 'with defaults' do + + it 'should contain all of the default resources' do + + should contain_class('rabbitmq::server').with( + :service_ensure => 'running', + :port => '5672', + :delete_guest_user => false + ) + + should contain_rabbitmq_vhost('/').with( + :provider => 'rabbitmqctl' + ) + end + + end + + describe 'when a rabbitmq user is specified' do + + let :params do + { + :userid => 'dan', + :password => 'pass' + } + end + + it 'should contain user and permissions' do + + should contain_rabbitmq_user('dan').with( + :admin => true, + :password => 'pass', + :provider => 'rabbitmqctl' + ) + + should contain_rabbitmq_user_permissions('dan@/').with( + :configure_permission => '.*', + :write_permission => '.*', + :read_permission => '.*', + :provider => 'rabbitmqctl' + ) + + end + + end + + describe 'when disabled' do + let :params do + { + :userid => 'dan', + :password => 'pass', + :enabled => false + } + end + + it 'should be disabled' do + + should_not contain_rabbitmq_user('dan') + should_not contain_rabbitmq_user_permissions('dan@/') + should contain_class('rabbitmq::server').with( + :service_ensure => 'stopped', + :port => '5672', + :delete_guest_user => false + ) + + should_not contain_rabbitmq_vhost('/') + + end + end + + +end diff --git a/cinder/spec/classes/cinder_scheduler_spec.rb b/cinder/spec/classes/cinder_scheduler_spec.rb new file mode 100644 index 000000000..4ce066a09 --- /dev/null +++ b/cinder/spec/classes/cinder_scheduler_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe 'cinder::scheduler' do + + describe 'on debian platforms' do + + let :facts do + { :osfamily => 'Debian' } + end + + describe 'with default parameters' do + + it { should contain_class('cinder::params') } + + it { should contain_package('cinder-scheduler').with( + :name => 'cinder-scheduler', + :ensure => 'present', + :before => 'Service[cinder-scheduler]' + ) } + + it { should contain_service('cinder-scheduler').with( + :name => 'cinder-scheduler', + :enable => true, + :ensure => 'running', + :require => 'Package[cinder]', + :hasstatus => true + ) } + end + + describe 'with parameters' do + + let :params do + { :scheduler_driver => 'cinder.scheduler.filter_scheduler.FilterScheduler', + :package_ensure => 'present' + } + end + + it { should contain_cinder_config('DEFAULT/scheduler_driver').with_value('cinder.scheduler.filter_scheduler.FilterScheduler') } + it { should contain_package('cinder-scheduler').with_ensure('present') } + end + + describe 'with manage_service false' do + let :params do + { 'manage_service' => false + } + end + it 'should not change the state of the service' do + should contain_service('cinder-scheduler').without_ensure + end + end + end + + + describe 'on rhel platforms' do + + let :facts do + { :osfamily => 'RedHat' } + end + + describe 'with default parameters' do + + it { should contain_class('cinder::params') } + + it { should contain_service('cinder-scheduler').with( + :name => 'openstack-cinder-scheduler', + :enable => true, + :ensure => 'running', + :require => 'Package[cinder]' + ) } + end + + describe 'with parameters' do + + let :params do + { :scheduler_driver => 'cinder.scheduler.filter_scheduler.FilterScheduler' } + end + + it { should contain_cinder_config('DEFAULT/scheduler_driver').with_value('cinder.scheduler.filter_scheduler.FilterScheduler') } + end + end +end diff --git a/cinder/spec/classes/cinder_setup_test_volume_spec.rb b/cinder/spec/classes/cinder_setup_test_volume_spec.rb new file mode 100644 index 000000000..678e196cf --- /dev/null +++ b/cinder/spec/classes/cinder_setup_test_volume_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe 'cinder::setup_test_volume' do + + it { should contain_package('lvm2').with( + :ensure => 'present' + ) } + + it { should contain_file('/var/lib/cinder').with( + :ensure => 'directory', + :require => 'Package[cinder]' + ) } + + it 'should contain volume creation execs' do + should contain_exec('create_/var/lib/cinder/cinder-volumes').with( + :command => 'dd if=/dev/zero of="/var/lib/cinder/cinder-volumes" bs=1 count=0 seek=4G' + ) + should contain_exec('losetup /dev/loop2 /var/lib/cinder/cinder-volumes') + should contain_exec('pvcreate /dev/loop2') + should contain_exec('vgcreate cinder-volumes /dev/loop2') + end +end diff --git a/cinder/spec/classes/cinder_spec.rb b/cinder/spec/classes/cinder_spec.rb new file mode 100644 index 000000000..8d22a62f6 --- /dev/null +++ b/cinder/spec/classes/cinder_spec.rb @@ -0,0 +1,347 @@ +require 'spec_helper' +describe 'cinder' do + let :req_params do + {:rabbit_password => 'guest', :database_connection => 'mysql://user:password@host/database'} + end + + let :facts do + {:osfamily => 'Debian'} + end + + describe 'with only required params' do + let :params do + req_params + end + + it { should contain_class('cinder::params') } + it { should contain_class('mysql::bindings::python') } + + it 'should contain default config' do + should contain_cinder_config('DEFAULT/rpc_backend').with( + :value => 'cinder.openstack.common.rpc.impl_kombu' + ) + should contain_cinder_config('DEFAULT/control_exchange').with( + :value => 'openstack' + ) + should contain_cinder_config('DEFAULT/rabbit_password').with( + :value => 'guest', + :secret => true + ) + should contain_cinder_config('DEFAULT/rabbit_host').with( + :value => '127.0.0.1' + ) + should contain_cinder_config('DEFAULT/rabbit_port').with( + :value => '5672' + ) + should contain_cinder_config('DEFAULT/rabbit_hosts').with( + :value => '127.0.0.1:5672' + ) + should contain_cinder_config('DEFAULT/rabbit_ha_queues').with( + :value => false + ) + should contain_cinder_config('DEFAULT/rabbit_virtual_host').with( + :value => '/' + ) + should contain_cinder_config('DEFAULT/rabbit_userid').with( + :value => 'guest' + ) + should contain_cinder_config('database/connection').with( + :value => 'mysql://user:password@host/database', + :secret => true + ) + should contain_cinder_config('database/idle_timeout').with( + :value => '3600' + ) + should contain_cinder_config('DEFAULT/verbose').with( + :value => false + ) + should contain_cinder_config('DEFAULT/debug').with( + :value => false + ) + should contain_cinder_config('DEFAULT/storage_availability_zone').with( + :value => 'nova' + ) + should contain_cinder_config('DEFAULT/default_availability_zone').with( + :value => 'nova' + ) + should contain_cinder_config('DEFAULT/api_paste_config').with( + :value => '/etc/cinder/api-paste.ini' + ) + should contain_cinder_config('DEFAULT/log_dir').with(:value => '/var/log/cinder') + end + + it { should contain_file('/etc/cinder/cinder.conf').with( + :owner => 'cinder', + :group => 'cinder', + :mode => '0600', + :require => 'Package[cinder]' + ) } + + it { should contain_file('/etc/cinder/api-paste.ini').with( + :owner => 'cinder', + :group => 'cinder', + :mode => '0600', + :require => 'Package[cinder]' + ) } + + end + describe 'with modified rabbit_hosts' do + let :params do + req_params.merge({'rabbit_hosts' => ['rabbit1:5672', 'rabbit2:5672']}) + end + + it 'should contain many' do + should_not contain_cinder_config('DEFAULT/rabbit_host') + should_not contain_cinder_config('DEFAULT/rabbit_port') + should contain_cinder_config('DEFAULT/rabbit_hosts').with( + :value => 'rabbit1:5672,rabbit2:5672' + ) + should contain_cinder_config('DEFAULT/rabbit_ha_queues').with( + :value => true + ) + end + end + + describe 'with a single rabbit_hosts entry' do + let :params do + req_params.merge({'rabbit_hosts' => ['rabbit1:5672']}) + end + + it 'should contain many' do + should_not contain_cinder_config('DEFAULT/rabbit_host') + should_not contain_cinder_config('DEFAULT/rabbit_port') + should contain_cinder_config('DEFAULT/rabbit_hosts').with( + :value => 'rabbit1:5672' + ) + should contain_cinder_config('DEFAULT/rabbit_ha_queues').with( + :value => true + ) + end + end + + describe 'with qpid rpc supplied' do + + let :params do + { + :database_connection => 'mysql://user:password@host/database', + :qpid_password => 'guest', + :rpc_backend => 'cinder.openstack.common.rpc.impl_qpid' + } + end + + it { should contain_cinder_config('database/connection').with_value('mysql://user:password@host/database') } + it { should contain_cinder_config('DEFAULT/rpc_backend').with_value('cinder.openstack.common.rpc.impl_qpid') } + it { should contain_cinder_config('DEFAULT/qpid_hostname').with_value('localhost') } + it { should contain_cinder_config('DEFAULT/qpid_port').with_value('5672') } + it { should contain_cinder_config('DEFAULT/qpid_username').with_value('guest') } + it { should contain_cinder_config('DEFAULT/qpid_password').with_value('guest').with_secret(true) } + it { should contain_cinder_config('DEFAULT/qpid_reconnect').with_value(true) } + it { should contain_cinder_config('DEFAULT/qpid_reconnect_timeout').with_value('0') } + it { should contain_cinder_config('DEFAULT/qpid_reconnect_limit').with_value('0') } + it { should contain_cinder_config('DEFAULT/qpid_reconnect_interval_min').with_value('0') } + it { should contain_cinder_config('DEFAULT/qpid_reconnect_interval_max').with_value('0') } + it { should contain_cinder_config('DEFAULT/qpid_reconnect_interval').with_value('0') } + it { should contain_cinder_config('DEFAULT/qpid_heartbeat').with_value('60') } + it { should contain_cinder_config('DEFAULT/qpid_protocol').with_value('tcp') } + it { should contain_cinder_config('DEFAULT/qpid_tcp_nodelay').with_value(true) } + end + + describe 'with qpid rpc and no qpid_sasl_mechanisms' do + let :params do + { + :database_connection => 'mysql://user:password@host/database', + :qpid_password => 'guest', + :rpc_backend => 'cinder.openstack.common.rpc.impl_qpid' + } + end + + it { should contain_cinder_config('DEFAULT/qpid_sasl_mechanisms').with_ensure('absent') } + end + + describe 'with qpid rpc and qpid_sasl_mechanisms string' do + let :params do + { + :database_connection => 'mysql://user:password@host/database', + :qpid_password => 'guest', + :qpid_sasl_mechanisms => 'PLAIN', + :rpc_backend => 'cinder.openstack.common.rpc.impl_qpid' + } + end + + it { should contain_cinder_config('DEFAULT/qpid_sasl_mechanisms').with_value('PLAIN') } + end + + describe 'with qpid rpc and qpid_sasl_mechanisms array' do + let :params do + { + :database_connection => 'mysql://user:password@host/database', + :qpid_password => 'guest', + :qpid_sasl_mechanisms => [ 'DIGEST-MD5', 'GSSAPI', 'PLAIN' ], + :rpc_backend => 'cinder.openstack.common.rpc.impl_qpid' + } + end + + it { should contain_cinder_config('DEFAULT/qpid_sasl_mechanisms').with_value('DIGEST-MD5 GSSAPI PLAIN') } + end + + describe 'with SSL enabled' do + let :params do + req_params.merge!({ + :rabbit_use_ssl => true, + :kombu_ssl_ca_certs => '/path/to/ssl/ca/certs', + :kombu_ssl_certfile => '/path/to/ssl/cert/file', + :kombu_ssl_keyfile => '/path/to/ssl/keyfile', + :kombu_ssl_version => 'SSLv3' + }) + end + + it do + should contain_cinder_config('DEFAULT/rabbit_use_ssl').with_value('true') + should contain_cinder_config('DEFAULT/kombu_ssl_ca_certs').with_value('/path/to/ssl/ca/certs') + should contain_cinder_config('DEFAULT/kombu_ssl_certfile').with_value('/path/to/ssl/cert/file') + should contain_cinder_config('DEFAULT/kombu_ssl_keyfile').with_value('/path/to/ssl/keyfile') + should contain_cinder_config('DEFAULT/kombu_ssl_version').with_value('SSLv3') + end + end + + describe 'with SSL disabled' do + let :params do + req_params.merge!({ + :rabbit_use_ssl => false, + :kombu_ssl_ca_certs => 'undef', + :kombu_ssl_certfile => 'undef', + :kombu_ssl_keyfile => 'undef', + :kombu_ssl_version => 'SSLv3' + }) + end + + it do + should contain_cinder_config('DEFAULT/rabbit_use_ssl').with_value('false') + should contain_cinder_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') + should contain_cinder_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') + should contain_cinder_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') + should contain_cinder_config('DEFAULT/kombu_ssl_version').with_ensure('absent') + end + end + + describe 'with syslog disabled' do + let :params do + req_params + end + + it { should contain_cinder_config('DEFAULT/use_syslog').with_value(false) } + end + + describe 'with syslog enabled' do + let :params do + req_params.merge({ + :use_syslog => 'true', + }) + end + + it { should contain_cinder_config('DEFAULT/use_syslog').with_value(true) } + it { should contain_cinder_config('DEFAULT/syslog_log_facility').with_value('LOG_USER') } + end + + describe 'with syslog enabled and custom settings' do + let :params do + req_params.merge({ + :use_syslog => 'true', + :log_facility => 'LOG_LOCAL0' + }) + end + + it { should contain_cinder_config('DEFAULT/use_syslog').with_value(true) } + it { should contain_cinder_config('DEFAULT/syslog_log_facility').with_value('LOG_LOCAL0') } + end + + describe 'with log_dir disabled' do + let(:params) { req_params.merge!({:log_dir => false}) } + it { should contain_cinder_config('DEFAULT/log_dir').with_ensure('absent') } + end + + describe 'with amqp_durable_queues disabled' do + let :params do + req_params + end + + it { should contain_cinder_config('DEFAULT/amqp_durable_queues').with_value(false) } + end + + describe 'with amqp_durable_queues enabled' do + let :params do + req_params.merge({ + :amqp_durable_queues => true, + }) + end + + it { should contain_cinder_config('DEFAULT/amqp_durable_queues').with_value(true) } + end + + describe 'with postgresql' do + let :params do + { + :database_connection => 'postgresql://user:drowssap@host/database', + :rabbit_password => 'guest', + } + end + + it { should contain_cinder_config('database/connection').with( + :value => 'postgresql://user:drowssap@host/database', + :secret => true + ) } + it { should_not contain_class('mysql::python') } + it { should_not contain_class('mysql::bindings') } + it { should_not contain_class('mysql::bindings::python') } + end + + describe 'with SSL socket options set' do + let :params do + { + :use_ssl => true, + :cert_file => '/path/to/cert', + :ca_file => '/path/to/ca', + :key_file => '/path/to/key', + :rabbit_password => 'guest', + } + end + + it { should contain_cinder_config('DEFAULT/ssl_ca_file').with_value('/path/to/ca') } + it { should contain_cinder_config('DEFAULT/ssl_cert_file').with_value('/path/to/cert') } + it { should contain_cinder_config('DEFAULT/ssl_key_file').with_value('/path/to/key') } + end + + describe 'with SSL socket options set to false' do + let :params do + { + :use_ssl => false, + :cert_file => false, + :ca_file => false, + :key_file => false, + :rabbit_password => 'guest', + } + end + + it { should contain_cinder_config('DEFAULT/ssl_ca_file').with_ensure('absent') } + it { should contain_cinder_config('DEFAULT/ssl_cert_file').with_ensure('absent') } + it { should contain_cinder_config('DEFAULT/ssl_key_file').with_ensure('absent') } + end + + describe 'with SSL socket options set wrongly configured' do + let :params do + { + :use_ssl => true, + :ca_file => '/path/to/ca', + :key_file => '/path/to/key', + :rabbit_password => 'guest', + } + end + + it 'should raise an error' do + expect { + should compile + }.to raise_error Puppet::Error, /The cert_file parameter is required when use_ssl is set to true/ + end + end + +end diff --git a/cinder/spec/classes/cinder_vmware_spec.rb b/cinder/spec/classes/cinder_vmware_spec.rb new file mode 100644 index 000000000..705613626 --- /dev/null +++ b/cinder/spec/classes/cinder_vmware_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'cinder::vmware' do + + let :params do + {:os_password => 'asdf', + :os_tenant_name => 'admin', + :os_username => 'admin', + :os_auth_url => 'http://127.127.127.1:5000/v2.0/'} + end + + describe 'with defaults' do + it 'should create vmware special types' do + should contain_cinder__type('vmware-thin').with( + :set_key => 'vmware:vmdk_type', + :set_value => 'thin') + + should contain_cinder__type('vmware-thick').with( + :set_key => 'vmware:vmdk_type', + :set_value => 'thick') + + should contain_cinder__type('vmware-eagerZeroedThick').with( + :set_key => 'vmware:vmdk_type', + :set_value => 'eagerZeroedThick') + + should contain_cinder__type('vmware-full').with( + :set_key => 'vmware:clone_type', + :set_value => 'full') + + should contain_cinder__type('vmware-linked').with( + :set_key => 'vmware:clone_type', + :set_value => 'linked') + end + end +end diff --git a/cinder/spec/classes/cinder_volume_eqlx_spec.rb b/cinder/spec/classes/cinder_volume_eqlx_spec.rb new file mode 100644 index 000000000..0f1a0ab8c --- /dev/null +++ b/cinder/spec/classes/cinder_volume_eqlx_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'cinder::volume::eqlx' do + + let :params do { + :san_ip => '192.168.100.10', + :san_login => 'grpadmin', + :san_password => '12345', + :san_thin_provision => true, + :eqlx_group_name => 'group-a', + :eqlx_pool => 'apool', + :eqlx_use_chap => true, + :eqlx_chap_login => 'chapadm', + :eqlx_chap_password => '56789', + :eqlx_cli_timeout => 31, + :eqlx_cli_max_retries => 6, + } + end + + describe 'eqlx volume driver' do + it 'configures eqlx volume driver' do + should contain_cinder_config('DEFAULT/volume_driver').with_value('cinder.volume.drivers.eqlx.DellEQLSanISCSIDriver') + should contain_cinder_config('DEFAULT/volume_backend_name').with_value('DEFAULT') + + params.each_pair do |config,value| + should contain_cinder_config("DEFAULT/#{config}").with_value(value) + end + end + + it 'marks eqlx_chap_password as secret' do + should contain_cinder_config('DEFAULT/eqlx_chap_password').with_secret( true ) + end + + end +end diff --git a/cinder/spec/classes/cinder_volume_glusterfs_spec.rb b/cinder/spec/classes/cinder_volume_glusterfs_spec.rb new file mode 100644 index 000000000..950d0c411 --- /dev/null +++ b/cinder/spec/classes/cinder_volume_glusterfs_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe 'cinder::volume::glusterfs' do + + shared_examples_for 'glusterfs volume driver' do + let :params do + { + :glusterfs_shares => ['10.10.10.10:/volumes', '10.10.10.11:/volumes'], + :glusterfs_shares_config => '/etc/cinder/other_shares.conf', + :glusterfs_sparsed_volumes => true, + :glusterfs_mount_point_base => '/cinder_mount_point', + } + end + + it 'configures glusterfs volume driver' do + should contain_cinder_config('DEFAULT/volume_driver').with_value( + 'cinder.volume.drivers.glusterfs.GlusterfsDriver') + should contain_cinder_config('DEFAULT/glusterfs_shares_config').with_value( + '/etc/cinder/other_shares.conf') + should contain_cinder_config('DEFAULT/glusterfs_sparsed_volumes').with_value( + true) + should contain_cinder_config('DEFAULT/glusterfs_mount_point_base').with_value( + '/cinder_mount_point') + should contain_file('/etc/cinder/other_shares.conf').with( + :content => "10.10.10.10:/volumes\n10.10.10.11:/volumes\n", + :require => 'Package[cinder]', + :notify => 'Service[cinder-volume]' + ) + end + + context "with an parameter which has been removed" do + before do + params.merge!({ + :glusterfs_disk_util => 'foo', + }) + end + it 'should fails' do + expect { subject }.to raise_error(Puppet::Error, /glusterfs_disk_util is removed in Icehouse./) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'glusterfs volume driver' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'glusterfs volume driver' + end + +end diff --git a/cinder/spec/classes/cinder_volume_iscsi_spec.rb b/cinder/spec/classes/cinder_volume_iscsi_spec.rb new file mode 100644 index 000000000..27a3c5345 --- /dev/null +++ b/cinder/spec/classes/cinder_volume_iscsi_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe 'cinder::volume::iscsi' do + + let :req_params do + {:iscsi_ip_address => '127.0.0.2'} + end + + let :facts do + {:osfamily => 'Debian'} + end + + describe 'with default params' do + + let :params do + req_params + end + + it { should contain_cinder_config('DEFAULT/iscsi_ip_address').with(:value => '127.0.0.2')} + it { should contain_cinder_config('DEFAULT/iscsi_helper').with(:value => 'tgtadm')} + it { should contain_cinder_config('DEFAULT/volume_group').with(:value => 'cinder-volumes')} + + end + + describe 'with a unsupported iscsi helper' do + let(:params) { req_params.merge(:iscsi_helper => 'fooboozoo')} + + it 'should raise an error' do + expect { + should compile + }.to raise_error Puppet::Error, /Unsupported iscsi helper: fooboozoo/ + end + end + + describe 'with RedHat' do + + let :params do + req_params + end + + let :facts do + {:osfamily => 'RedHat'} + end + + it { should contain_file_line('cinder include').with( + :line => 'include /etc/cinder/volumes/*', + :path => '/etc/tgt/targets.conf' + ) } + + end + + describe 'with lioadm' do + + let :params do { + :iscsi_ip_address => '127.0.0.2', + :iscsi_helper => 'lioadm' + } + end + + let :facts do + {:osfamily => 'RedHat'} + end + + it { should contain_package('targetcli').with_ensure('present')} + it { should contain_service('target').with( + :ensure => 'running', + :enable => 'true', + :require => 'Package[targetcli]' + ) } + + end + +end diff --git a/cinder/spec/classes/cinder_volume_netapp_spec.rb b/cinder/spec/classes/cinder_volume_netapp_spec.rb new file mode 100644 index 000000000..8c9a1dbc7 --- /dev/null +++ b/cinder/spec/classes/cinder_volume_netapp_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe 'cinder::volume::netapp' do + + let :params do + { + :netapp_login => 'netapp', + :netapp_password => 'password', + :netapp_server_hostname => '127.0.0.2', + } + end + + let :default_params do + { + :netapp_server_port => '80', + :netapp_size_multiplier => '1.2', + :netapp_storage_family => 'ontap_cluster', + :netapp_storage_protocol => 'nfs', + :netapp_transport_type => 'http', + :netapp_vfiler => '', + :netapp_volume_list => '', + :netapp_vserver => '', + :expiry_thres_minutes => '720', + :thres_avl_size_perc_start => '20', + :thres_avl_size_perc_stop => '60', + :nfs_shares_config => '', + :netapp_copyoffload_tool_path => '', + :netapp_controller_ips => '', + :netapp_sa_password => '', + :netapp_storage_pools => '', + :netapp_webservice_path => '/devmgr/v2', + } + end + + + shared_examples_for 'netapp volume driver' do + let :params_hash do + default_params.merge(params) + end + + it 'configures netapp volume driver' do + should contain_cinder_config('DEFAULT/volume_driver').with_value( + 'cinder.volume.drivers.netapp.common.NetAppDriver') + params_hash.each_pair do |config,value| + should contain_cinder_config("DEFAULT/#{config}").with_value( value ) + end + end + + it 'marks netapp_password as secret' do + should contain_cinder_config('DEFAULT/netapp_password').with_secret( true ) + end + + it 'marks netapp_sa_password as secret' do + should contain_cinder_config('DEFAULT/netapp_sa_password').with_secret( true ) + end + end + + + context 'with default parameters' do + before do + params = {} + end + + it_configures 'netapp volume driver' + end + + context 'with provided parameters' do + it_configures 'netapp volume driver' + end +end diff --git a/cinder/spec/classes/cinder_volume_nexenta_spec.rb b/cinder/spec/classes/cinder_volume_nexenta_spec.rb new file mode 100644 index 000000000..258400fae --- /dev/null +++ b/cinder/spec/classes/cinder_volume_nexenta_spec.rb @@ -0,0 +1,43 @@ +# author 'Aimon Bustardo ' +# license 'Apache License 2.0' +# description 'configures openstack cinder nexenta driver' +require 'spec_helper' + +describe 'cinder::volume::nexenta' do + + let :params do + { :nexenta_user => 'nexenta', + :nexenta_password => 'password', + :nexenta_host => '127.0.0.2' } + end + + let :default_params do + { :nexenta_volume => 'cinder', + :nexenta_target_prefix => 'iqn:', + :nexenta_target_group_prefix => 'cinder/', + :nexenta_blocksize => '8k', + :nexenta_sparse => true } + end + + let :facts do + { :osfamily => 'Debian' } + end + + + context 'with required params' do + let :params_hash do + default_params.merge(params) + end + + it 'configures nexenta volume driver' do + params_hash.each_pair do |config, value| + should contain_cinder_config("DEFAULT/#{config}").with_value(value) + end + end + + it 'marks nexenta_password as secret' do + should contain_cinder_config('DEFAULT/nexenta_password').with_secret( true ) + end + + end +end diff --git a/cinder/spec/classes/cinder_volume_nfs_spec.rb b/cinder/spec/classes/cinder_volume_nfs_spec.rb new file mode 100644 index 000000000..449153fcd --- /dev/null +++ b/cinder/spec/classes/cinder_volume_nfs_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'cinder::volume::nfs' do + + let :params do + { + :nfs_servers => ['10.10.10.10:/shares', '10.10.10.10:/shares2'], + :nfs_mount_options => 'vers=3', + :nfs_shares_config => '/etc/cinder/other_shares.conf', + :nfs_disk_util => 'du', + :nfs_sparsed_volumes => true, + :nfs_mount_point_base => '/cinder_mount_point', + :nfs_used_ratio => '0.95', + :nfs_oversub_ratio => '1.0', + } + end + + describe 'nfs volume driver' do + it 'configures nfs volume driver' do + should contain_cinder_config('DEFAULT/volume_driver').with_value( + 'cinder.volume.drivers.nfs.NfsDriver') + should contain_cinder_config('DEFAULT/nfs_shares_config').with_value( + '/etc/cinder/other_shares.conf') + should contain_cinder_config('DEFAULT/nfs_mount_options').with_value( + 'vers=3') + should contain_cinder_config('DEFAULT/nfs_sparsed_volumes').with_value( + true) + should contain_cinder_config('DEFAULT/nfs_mount_point_base').with_value( + '/cinder_mount_point') + should contain_cinder_config('DEFAULT/nfs_disk_util').with_value( + 'du') + should contain_cinder_config('DEFAULT/nfs_used_ratio').with_value( + '0.95') + should contain_cinder_config('DEFAULT/nfs_oversub_ratio').with_value( + '1.0') + should contain_file('/etc/cinder/other_shares.conf').with( + :content => "10.10.10.10:/shares\n10.10.10.10:/shares2", + :require => 'Package[cinder]', + :notify => 'Service[cinder-volume]' + ) + end + end +end diff --git a/cinder/spec/classes/cinder_volume_rbd_spec.rb b/cinder/spec/classes/cinder_volume_rbd_spec.rb new file mode 100644 index 000000000..ec3a92677 --- /dev/null +++ b/cinder/spec/classes/cinder_volume_rbd_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe 'cinder::volume::rbd' do + let :req_params do + { + :rbd_pool => 'volumes', + :glance_api_version => '2', + :rbd_user => 'test', + :rbd_secret_uuid => '0123456789', + :rbd_ceph_conf => '/foo/boo/zoo/ceph.conf', + :rbd_flatten_volume_from_snapshot => true, + :volume_tmp_dir => '/foo/tmp', + :rbd_max_clone_depth => '0' + } + end + + it { should contain_class('cinder::params') } + + let :params do + req_params + end + + let :facts do + {:osfamily => 'Debian'} + end + + describe 'rbd volume driver' do + it 'configure rbd volume driver' do + should contain_cinder_config('DEFAULT/volume_driver').with_value('cinder.volume.drivers.rbd.RBDDriver') + + should contain_cinder_config('DEFAULT/rbd_ceph_conf').with_value(req_params[:rbd_ceph_conf]) + should contain_cinder_config('DEFAULT/rbd_flatten_volume_from_snapshot').with_value(req_params[:rbd_flatten_volume_from_snapshot]) + should contain_cinder_config('DEFAULT/volume_tmp_dir').with_value(req_params[:volume_tmp_dir]) + should contain_cinder_config('DEFAULT/rbd_max_clone_depth').with_value(req_params[:rbd_max_clone_depth]) + should contain_cinder_config('DEFAULT/rbd_pool').with_value(req_params[:rbd_pool]) + should contain_cinder_config('DEFAULT/rbd_user').with_value(req_params[:rbd_user]) + should contain_cinder_config('DEFAULT/rbd_secret_uuid').with_value(req_params[:rbd_secret_uuid]) + should contain_file('/etc/init/cinder-volume.override').with(:ensure => 'present') + should contain_file_line('set initscript env').with( + :line => /env CEPH_ARGS=\"--id test\"/, + :path => '/etc/init/cinder-volume.override', + :notify => 'Service[cinder-volume]') + end + + context 'with rbd_secret_uuid disabled' do + let(:params) { req_params.merge!({:rbd_secret_uuid => false}) } + it { should contain_cinder_config('DEFAULT/rbd_secret_uuid').with_ensure('absent') } + end + + context 'with volume_tmp_dir disabled' do + let(:params) { req_params.merge!({:volume_tmp_dir => false}) } + it { should contain_cinder_config('DEFAULT/volume_tmp_dir').with_ensure('absent') } + end + + end + + + describe 'with RedHat' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :params do + req_params + end + + it 'should ensure that the cinder-volume sysconfig file is present' do + should contain_file('/etc/sysconfig/openstack-cinder-volume').with( + :ensure => 'present' + ) + end + + it 'should configure RedHat init override' do + should contain_file_line('set initscript env').with( + :line => /export CEPH_ARGS=\"--id test\"/, + :path => '/etc/sysconfig/openstack-cinder-volume', + :notify => 'Service[cinder-volume]') + end + end + +end + diff --git a/cinder/spec/classes/cinder_volume_san_spec.rb b/cinder/spec/classes/cinder_volume_san_spec.rb new file mode 100644 index 000000000..f843fd915 --- /dev/null +++ b/cinder/spec/classes/cinder_volume_san_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'cinder::volume::san' do + + let :params do + { :volume_driver => 'cinder.volume.san.SolarisISCSIDriver', + :san_ip => '127.0.0.1', + :san_login => 'cluster_operator', + :san_password => '007', + :san_clustername => 'storage_cluster' } + end + + let :default_params do + { :san_thin_provision => true, + :san_login => 'admin', + :san_ssh_port => 22, + :san_is_local => false, + :ssh_conn_timeout => 30, + :ssh_min_pool_conn => 1, + :ssh_max_pool_conn => 5 } + end + + shared_examples_for 'a san volume driver' do + let :params_hash do + default_params.merge(params) + end + + it 'configures cinder volume driver' do + params_hash.each_pair do |config,value| + should contain_cinder_config("DEFAULT/#{config}").with_value( value ) + end + end + + it 'marks san_password as secret' do + should contain_cinder_config('DEFAULT/san_password').with_secret( true ) + end + + end + + + context 'with parameters' do + it_configures 'a san volume driver' + end +end diff --git a/cinder/spec/classes/cinder_volume_solidfire_spec.rb b/cinder/spec/classes/cinder_volume_solidfire_spec.rb new file mode 100644 index 000000000..7b5e1303d --- /dev/null +++ b/cinder/spec/classes/cinder_volume_solidfire_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'cinder::volume::solidfire' do + let :req_params do + { + :san_ip => '127.0.0.2', + :san_login => 'solidfire', + :san_password => 'password', + } + end + + let :params do + req_params + end + + describe 'solidfire volume driver' do + it 'configure solidfire volume driver' do + should contain_cinder_config('DEFAULT/volume_driver').with_value('cinder.volume.drivers.solidfire.SolidFire') + should contain_cinder_config('DEFAULT/san_ip').with_value('127.0.0.2') + should contain_cinder_config('DEFAULT/san_login').with_value('solidfire') + should contain_cinder_config('DEFAULT/san_password').with_value('password') + end + + it 'marks san_password as secret' do + should contain_cinder_config('DEFAULT/san_password').with_secret( true ) + end + + end +end diff --git a/cinder/spec/classes/cinder_volume_spec.rb b/cinder/spec/classes/cinder_volume_spec.rb new file mode 100644 index 000000000..4b3cc14bd --- /dev/null +++ b/cinder/spec/classes/cinder_volume_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'cinder::volume' do + + let :pre_condition do + 'class { "cinder": rabbit_password => "fpp", database_connection => "mysql://a:b@c/d" }' + end + + let :facts do + {:osfamily => 'Debian'} + end + + it { should contain_package('cinder-volume').with_ensure('present') } + it { should contain_service('cinder-volume').with( + 'hasstatus' => true + )} + + describe 'with manage_service false' do + let :params do + { 'manage_service' => false } + end + it 'should not change the state of the service' do + should contain_service('cinder-volume').without_ensure + end + end +end diff --git a/cinder/spec/classes/cinder_volume_vmdk_spec.rb b/cinder/spec/classes/cinder_volume_vmdk_spec.rb new file mode 100644 index 000000000..8aa364c3f --- /dev/null +++ b/cinder/spec/classes/cinder_volume_vmdk_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe 'cinder::volume::vmdk' do + + let :params do + { + :host_ip => '172.16.16.16', + :host_password => 'asdf', + :host_username => 'user' + } + end + + let :optional_params do + { + :volume_folder => 'cinder-volume-folder', + :api_retry_count => 5, + :max_object_retrieval => 200, + :task_poll_interval => 10, + :image_transfer_timeout_secs => 3600, + :wsdl_location => 'http://127.0.0.1:8080/vmware/SDK/wsdl/vim25/vimService.wsdl' + } + end + + it 'should configure vmdk driver in cinder.conf' do + should contain_cinder_config('DEFAULT/volume_driver').with_value('cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver') + should contain_cinder_config('DEFAULT/vmware_host_ip').with_value(params[:host_ip]) + should contain_cinder_config('DEFAULT/vmware_host_username').with_value(params[:host_username]) + should contain_cinder_config('DEFAULT/vmware_host_password').with_value(params[:host_password]) + should contain_cinder_config('DEFAULT/vmware_volume_folder').with_value('cinder-volumes') + should contain_cinder_config('DEFAULT/vmware_api_retry_count').with_value(10) + should contain_cinder_config('DEFAULT/vmware_max_object_retrieval').with_value(100) + should contain_cinder_config('DEFAULT/vmware_task_poll_interval').with_value(5) + should contain_cinder_config('DEFAULT/vmware_image_transfer_timeout_secs').with_value(7200) + should_not contain_cinder_config('DEFAULT/vmware_wsdl_location') + end + + it 'marks vmware_host_password as secret' do + should contain_cinder_config('DEFAULT/vmware_host_password').with_secret( true ) + end + + it 'installs vmdk python driver' do + should contain_package('python-suds').with( + :ensure => 'present' + ) + end + + context 'with optional parameters' do + before :each do + params.merge!(optional_params) + end + + it 'should configure vmdk driver in cinder.conf' do + should contain_cinder_config('DEFAULT/vmware_volume_folder').with_value(params[:volume_folder]) + should contain_cinder_config('DEFAULT/vmware_api_retry_count').with_value(params[:api_retry_count]) + should contain_cinder_config('DEFAULT/vmware_max_object_retrieval').with_value(params[:max_object_retrieval]) + should contain_cinder_config('DEFAULT/vmware_task_poll_interval').with_value(params[:task_poll_interval]) + should contain_cinder_config('DEFAULT/vmware_image_transfer_timeout_secs').with_value(params[:image_transfer_timeout_secs]) + should contain_cinder_config('DEFAULT/vmware_wsdl_location').with_value(params[:wsdl_location]) + end + end +end diff --git a/cinder/spec/defines/cinder_backend_eqlx_spec.rb b/cinder/spec/defines/cinder_backend_eqlx_spec.rb new file mode 100644 index 000000000..a6a287a9d --- /dev/null +++ b/cinder/spec/defines/cinder_backend_eqlx_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'cinder::backend::eqlx' do + let (:config_group_name) { 'eqlx-1' } + + let (:title) { config_group_name } + + let :params do + { + :san_ip => '192.168.100.10', + :san_login => 'grpadmin', + :san_password => '12345', + :volume_backend_name => 'Dell_EQLX', + :san_thin_provision => true, + :eqlx_group_name => 'group-a', + :eqlx_pool => 'apool', + :eqlx_use_chap => true, + :eqlx_chap_login => 'chapadm', + :eqlx_chap_password => '56789', + :eqlx_cli_timeout => 31, + :eqlx_cli_max_retries => 6, + } + end + + describe 'eqlx volume driver' do + it 'configure eqlx volume driver' do + should contain_cinder_config( + "#{config_group_name}/volume_driver").with_value( + 'cinder.volume.drivers.eqlx.DellEQLSanISCSIDriver') + params.each_pair do |config,value| + should contain_cinder_config( + "#{config_group_name}/#{config}").with_value(value) + end + end + end +end diff --git a/cinder/spec/defines/cinder_backend_glusterfs_spec.rb b/cinder/spec/defines/cinder_backend_glusterfs_spec.rb new file mode 100644 index 000000000..c0b4fa2ef --- /dev/null +++ b/cinder/spec/defines/cinder_backend_glusterfs_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe 'cinder::backend::glusterfs' do + + shared_examples_for 'glusterfs volume driver' do + let(:title) {'mygluster'} + + let :params do + { + :glusterfs_shares => ['10.10.10.10:/volumes', '10.10.10.11:/volumes'], + :glusterfs_shares_config => '/etc/cinder/other_shares.conf', + :glusterfs_sparsed_volumes => true, + :glusterfs_mount_point_base => '/cinder_mount_point', + } + end + + it 'configures glusterfs volume driver' do + should contain_cinder_config('mygluster/volume_driver').with_value( + 'cinder.volume.drivers.glusterfs.GlusterfsDriver') + should contain_cinder_config('mygluster/glusterfs_shares_config').with_value( + '/etc/cinder/other_shares.conf') + should contain_cinder_config('mygluster/glusterfs_sparsed_volumes').with_value( + true) + should contain_cinder_config('mygluster/glusterfs_mount_point_base').with_value( + '/cinder_mount_point') + should contain_file('/etc/cinder/other_shares.conf').with( + :content => "10.10.10.10:/volumes\n10.10.10.11:/volumes\n", + :require => 'Package[cinder]', + :notify => 'Service[cinder-volume]' + ) + end + + context "with an parameter which has been removed" do + before do + params.merge!({ + :glusterfs_disk_util => 'foo', + }) + end + it 'should fails' do + expect { subject }.to raise_error(Puppet::Error, /glusterfs_disk_util is removed in Icehouse./) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'glusterfs volume driver' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'glusterfs volume driver' + end + +end diff --git a/cinder/spec/defines/cinder_backend_iscsi_spec.rb b/cinder/spec/defines/cinder_backend_iscsi_spec.rb new file mode 100644 index 000000000..4e987648d --- /dev/null +++ b/cinder/spec/defines/cinder_backend_iscsi_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe 'cinder::backend::iscsi' do + + let(:title) {'hippo'} + + let :req_params do { + :iscsi_ip_address => '127.0.0.2', + :iscsi_helper => 'tgtadm', + } + end + + let :facts do + {:osfamily => 'Debian'} + end + + let :params do + req_params + end + + describe 'with default params' do + + it 'should configure iscsi driver' do + should contain_cinder_config('hippo/volume_backend_name').with( + :value => 'hippo') + should contain_cinder_config('hippo/iscsi_ip_address').with( + :value => '127.0.0.2') + should contain_cinder_config('hippo/iscsi_helper').with( + :value => 'tgtadm') + should contain_cinder_config('hippo/volume_group').with( + :value => 'cinder-volumes') + end + end + + describe 'with RedHat' do + + let :facts do + {:osfamily => 'RedHat'} + end + + it { should contain_file_line('cinder include').with( + :line => 'include /etc/cinder/volumes/*', + :path => '/etc/tgt/targets.conf' + ) } + + end + +end diff --git a/cinder/spec/defines/cinder_backend_netapp_spec.rb b/cinder/spec/defines/cinder_backend_netapp_spec.rb new file mode 100644 index 000000000..e0e316fc1 --- /dev/null +++ b/cinder/spec/defines/cinder_backend_netapp_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe 'cinder::backend::netapp' do + + let(:title) {'hippo'} + + let :params do + { + :volume_backend_name => 'netapp-cdot-nfs', + :netapp_login => 'netapp', + :netapp_password => 'password', + :netapp_server_hostname => '127.0.0.2', + } + end + + let :default_params do + { + :netapp_server_port => '80', + :netapp_size_multiplier => '1.2', + :netapp_storage_family => 'ontap_cluster', + :netapp_storage_protocol => 'nfs', + :netapp_transport_type => 'http', + :netapp_vfiler => '', + :netapp_volume_list => '', + :netapp_vserver => '', + :expiry_thres_minutes => '720', + :thres_avl_size_perc_start => '20', + :thres_avl_size_perc_stop => '60', + :nfs_shares_config => '', + :netapp_copyoffload_tool_path => '', + :netapp_controller_ips => '', + :netapp_sa_password => '', + :netapp_storage_pools => '', + :netapp_webservice_path => '/devmgr/v2', + } + end + + shared_examples_for 'netapp volume driver' do + let :params_hash do + default_params.merge(params) + end + + it 'configures netapp volume driver' do + should contain_cinder_config("#{params_hash[:volume_backend_name]}/volume_driver").with_value( + 'cinder.volume.drivers.netapp.common.NetAppDriver') + params_hash.each_pair do |config,value| + should contain_cinder_config("#{params_hash[:volume_backend_name]}/#{config}").with_value( value ) + end + end + + it 'marks netapp_password as secret' do + should contain_cinder_config("#{params_hash[:volume_backend_name]}/netapp_password").with_secret( true ) + end + + it 'marks netapp_sa_password as secret' do + should contain_cinder_config("#{params_hash[:volume_backend_name]}/netapp_sa_password").with_secret( true ) + end + end + + + context 'with default parameters' do + before do + params = {} + end + + it_configures 'netapp volume driver' + end + + context 'with provided parameters' do + it_configures 'netapp volume driver' + end + +end diff --git a/cinder/spec/defines/cinder_backend_nexenta_spec.rb b/cinder/spec/defines/cinder_backend_nexenta_spec.rb new file mode 100644 index 000000000..2c26c5865 --- /dev/null +++ b/cinder/spec/defines/cinder_backend_nexenta_spec.rb @@ -0,0 +1,39 @@ +# author 'Aimon Bustardo ' +# license 'Apache License 2.0' +# description 'configures openstack cinder nexenta driver' +require 'spec_helper' + +describe 'cinder::backend::nexenta' do + let (:title) { 'nexenta' } + + let :params do + { :nexenta_user => 'nexenta', + :nexenta_password => 'password', + :nexenta_host => '127.0.0.2' } + end + + let :default_params do + { :nexenta_volume => 'cinder', + :nexenta_target_prefix => 'iqn:', + :nexenta_target_group_prefix => 'cinder/', + :nexenta_blocksize => '8k', + :nexenta_sparse => true } + end + + let :facts do + { :osfamily => 'Debian' } + end + + + context 'with required params' do + let :params_hash do + default_params.merge(params) + end + + it 'configures nexenta volume driver' do + params_hash.each_pair do |config, value| + should contain_cinder_config("nexenta/#{config}").with_value(value) + end + end + end +end diff --git a/cinder/spec/defines/cinder_backend_nfs_spec.rb b/cinder/spec/defines/cinder_backend_nfs_spec.rb new file mode 100644 index 000000000..bc7906f6d --- /dev/null +++ b/cinder/spec/defines/cinder_backend_nfs_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe 'cinder::backend::nfs' do + + let(:title) {'hippo'} + + let :params do + { + :nfs_servers => ['10.10.10.10:/shares', '10.10.10.10:/shares2'], + :nfs_mount_options => 'vers=3', + :nfs_shares_config => '/etc/cinder/other_shares.conf', + :nfs_disk_util => 'du', + :nfs_sparsed_volumes => true, + :nfs_mount_point_base => '/cinder_mount_point', + :nfs_used_ratio => '0.7', + :nfs_oversub_ratio => '0.9' + } + end + + describe 'nfs volume driver' do + + it 'configures nfs volume driver' do + should contain_cinder_config('hippo/volume_backend_name').with( + :value => 'hippo') + should contain_cinder_config('hippo/volume_driver').with_value( + 'cinder.volume.drivers.nfs.NfsDriver') + should contain_cinder_config('hippo/nfs_shares_config').with_value( + '/etc/cinder/other_shares.conf') + should contain_cinder_config('hippo/nfs_mount_options').with_value( + 'vers=3') + should contain_cinder_config('hippo/nfs_sparsed_volumes').with_value( + true) + should contain_cinder_config('hippo/nfs_mount_point_base').with_value( + '/cinder_mount_point') + should contain_cinder_config('hippo/nfs_disk_util').with_value( + 'du') + should contain_cinder_config('hippo/nfs_used_ratio').with_value( + '0.7') + should contain_cinder_config('hippo/nfs_oversub_ratio').with_value( + '0.9') + should contain_file('/etc/cinder/other_shares.conf').with( + :content => "10.10.10.10:/shares\n10.10.10.10:/shares2", + :require => 'Package[cinder]', + :notify => 'Service[cinder-volume]' + ) + end + end +end diff --git a/cinder/spec/defines/cinder_backend_rbd_spec.rb b/cinder/spec/defines/cinder_backend_rbd_spec.rb new file mode 100644 index 000000000..0636344c0 --- /dev/null +++ b/cinder/spec/defines/cinder_backend_rbd_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +describe 'cinder::backend::rbd' do + + let(:title) {'rbd-ssd'} + + let :req_params do + { + :volume_backend_name => 'rbd-ssd', + :rbd_pool => 'volumes', + :glance_api_version => '2', + :rbd_user => 'test', + :rbd_secret_uuid => '0123456789', + :rbd_ceph_conf => '/foo/boo/zoo/ceph.conf', + :rbd_flatten_volume_from_snapshot => true, + :volume_tmp_dir => '/foo/tmp', + :rbd_max_clone_depth => '0' + } + end + + it { should contain_class('cinder::params') } + + let :params do + req_params + end + + let :facts do + {:osfamily => 'Debian'} + end + + describe 'rbd backend volume driver' do + it 'configure rbd volume driver' do + should contain_cinder_config("#{req_params[:volume_backend_name]}/volume_backend_name").with_value(req_params[:volume_backend_name]) + should contain_cinder_config("#{req_params[:volume_backend_name]}/volume_driver").with_value('cinder.volume.drivers.rbd.RBDDriver') + should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_ceph_conf").with_value(req_params[:rbd_ceph_conf]) + should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_flatten_volume_from_snapshot").with_value(req_params[:rbd_flatten_volume_from_snapshot]) + should contain_cinder_config("#{req_params[:volume_backend_name]}/volume_tmp_dir").with_value(req_params[:volume_tmp_dir]) + should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_max_clone_depth").with_value(req_params[:rbd_max_clone_depth]) + should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_pool").with_value(req_params[:rbd_pool]) + should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_user").with_value(req_params[:rbd_user]) + should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_secret_uuid").with_value(req_params[:rbd_secret_uuid]) + should contain_file('/etc/init/cinder-volume.override').with(:ensure => 'present') + should contain_file_line('set initscript env').with( + :line => /env CEPH_ARGS=\"--id test\"/, + :path => '/etc/init/cinder-volume.override', + :notify => 'Service[cinder-volume]') + end + + context 'with rbd_secret_uuid disabled' do + let(:params) { req_params.merge!({:rbd_secret_uuid => false}) } + it { should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_secret_uuid").with_ensure('absent') } + end + + context 'with volume_tmp_dir disabled' do + let(:params) { req_params.merge!({:volume_tmp_dir => false}) } + it { should contain_cinder_config("#{req_params[:volume_backend_name]}/volume_tmp_dir").with_ensure('absent') } + end + + context 'with another RBD backend' do + let :pre_condition do + "cinder::backend::rbd { 'ceph2': + rbd_pool => 'volumes2', + rbd_user => 'test' + }" + end + it { should contain_cinder_config("#{req_params[:volume_backend_name]}/volume_driver").with_value('cinder.volume.drivers.rbd.RBDDriver') } + it { should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_pool").with_value(req_params[:rbd_pool]) } + it { should contain_cinder_config("#{req_params[:volume_backend_name]}/rbd_user").with_value(req_params[:rbd_user]) } + it { should contain_cinder_config("ceph2/volume_driver").with_value('cinder.volume.drivers.rbd.RBDDriver') } + it { should contain_cinder_config("ceph2/rbd_pool").with_value('volumes2') } + it { should contain_cinder_config("ceph2/rbd_user").with_value('test') } + end + end + + describe 'with RedHat' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :params do + req_params + end + + it 'should ensure that the cinder-volume sysconfig file is present' do + should contain_file('/etc/sysconfig/openstack-cinder-volume').with( + :ensure => 'present' + ) + end + + it 'should configure RedHat init override' do + should contain_file_line('set initscript env').with( + :line => /export CEPH_ARGS=\"--id test\"/, + :path => '/etc/sysconfig/openstack-cinder-volume', + :notify => 'Service[cinder-volume]') + end + end + +end diff --git a/cinder/spec/defines/cinder_backend_san_spec.rb b/cinder/spec/defines/cinder_backend_san_spec.rb new file mode 100644 index 000000000..ef647de29 --- /dev/null +++ b/cinder/spec/defines/cinder_backend_san_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'cinder::backend::san' do + let (:title) { 'mysan' } + + let :params do + { :volume_driver => 'cinder.volume.san.SolarisISCSIDriver', + :san_ip => '127.0.0.1', + :san_login => 'cluster_operator', + :san_password => '007', + :san_clustername => 'storage_cluster' } + end + + let :default_params do + { :san_thin_provision => true, + :san_login => 'admin', + :san_ssh_port => 22, + :san_is_local => false, + :ssh_conn_timeout => 30, + :ssh_min_pool_conn => 1, + :ssh_max_pool_conn => 5 } + end + + shared_examples_for 'a san volume driver' do + let :params_hash do + default_params.merge(params) + end + + it 'configures cinder volume driver' do + params_hash.each_pair do |config,value| + should contain_cinder_config("mysan/#{config}").with_value( value ) + end + end + end + + + context 'with parameters' do + it_configures 'a san volume driver' + end +end diff --git a/cinder/spec/defines/cinder_backend_solidfire_spec.rb b/cinder/spec/defines/cinder_backend_solidfire_spec.rb new file mode 100644 index 000000000..5b35e1e7e --- /dev/null +++ b/cinder/spec/defines/cinder_backend_solidfire_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe 'cinder::backend::solidfire' do + let (:title) { 'solidfire' } + + let :req_params do + { + :san_ip => '127.0.0.2', + :san_login => 'solidfire', + :san_password => 'password', + } + end + + let :params do + req_params + end + + describe 'solidfire volume driver' do + it 'configure solidfire volume driver' do + should contain_cinder_config('solidfire/volume_driver').with_value( + 'cinder.volume.drivers.solidfire.SolidFire') + should contain_cinder_config('solidfire/san_ip').with_value( + '127.0.0.2') + should contain_cinder_config('solidfire/san_login').with_value( + 'solidfire') + should contain_cinder_config('solidfire/san_password').with_value( + 'password') + end + end +end diff --git a/cinder/spec/defines/cinder_backend_vmdk_spec.rb b/cinder/spec/defines/cinder_backend_vmdk_spec.rb new file mode 100644 index 000000000..85940acaa --- /dev/null +++ b/cinder/spec/defines/cinder_backend_vmdk_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe 'cinder::backend::vmdk' do + + let(:title) { 'hippo' } + + let :params do + { + :host_ip => '172.16.16.16', + :host_password => 'asdf', + :host_username => 'user' + } + end + + let :optional_params do + { + :volume_folder => 'cinder-volume-folder', + :api_retry_count => 5, + :max_object_retrieval => 200, + :task_poll_interval => 10, + :image_transfer_timeout_secs => 3600, + :wsdl_location => 'http://127.0.0.1:8080/vmware/SDK/wsdl/vim25/vimService.wsdl' + } + end + + it 'should configure vmdk driver in cinder.conf' do + should contain_cinder_config('hippo/volume_backend_name').with_value('hippo') + should contain_cinder_config('hippo/volume_driver').with_value('cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver') + should contain_cinder_config('hippo/vmware_host_ip').with_value(params[:host_ip]) + should contain_cinder_config('hippo/vmware_host_username').with_value(params[:host_username]) + should contain_cinder_config('hippo/vmware_host_password').with_value(params[:host_password]) + should contain_cinder_config('hippo/vmware_volume_folder').with_value('cinder-volumes') + should contain_cinder_config('hippo/vmware_api_retry_count').with_value(10) + should contain_cinder_config('hippo/vmware_max_object_retrieval').with_value(100) + should contain_cinder_config('hippo/vmware_task_poll_interval').with_value(5) + should contain_cinder_config('hippo/vmware_image_transfer_timeout_secs').with_value(7200) + should_not contain_cinder_config('hippo/vmware_wsdl_location') + end + + it 'installs suds python package' do + should contain_package('python-suds').with( + :ensure => 'present') + end + + context 'with optional parameters' do + before :each do + params.merge!(optional_params) + end + + it 'should configure vmdk driver in cinder.conf' do + should contain_cinder_config('hippo/vmware_volume_folder').with_value(params[:volume_folder]) + should contain_cinder_config('hippo/vmware_api_retry_count').with_value(params[:api_retry_count]) + should contain_cinder_config('hippo/vmware_max_object_retrieval').with_value(params[:max_object_retrieval]) + should contain_cinder_config('hippo/vmware_task_poll_interval').with_value(params[:task_poll_interval]) + should contain_cinder_config('hippo/vmware_image_transfer_timeout_secs').with_value(params[:image_transfer_timeout_secs]) + should contain_cinder_config('hippo/vmware_wsdl_location').with_value(params[:wsdl_location]) + end + end +end diff --git a/cinder/spec/defines/cinder_type_set_spec.rb b/cinder/spec/defines/cinder_type_set_spec.rb new file mode 100644 index 000000000..4cb57c189 --- /dev/null +++ b/cinder/spec/defines/cinder_type_set_spec.rb @@ -0,0 +1,29 @@ +#Author: Andrew Woodward + +require 'spec_helper' + +describe 'cinder::type_set' do + + let(:title) {'hippo'} + + let :params do { + :type => 'sith', + :key => 'monchichi', + :os_password => 'asdf', + :os_tenant_name => 'admin', + :os_username => 'admin', + :os_auth_url => 'http://127.127.127.1:5000/v2.0/', + } + end + + it 'should have its execs' do + should contain_exec('cinder type-key sith set monchichi=hippo').with( + :command => 'cinder type-key sith set monchichi=hippo', + :environment => [ + 'OS_TENANT_NAME=admin', + 'OS_USERNAME=admin', + 'OS_PASSWORD=asdf', + 'OS_AUTH_URL=http://127.127.127.1:5000/v2.0/'], + :require => 'Package[python-cinderclient]') + end +end diff --git a/cinder/spec/defines/cinder_type_spec.rb b/cinder/spec/defines/cinder_type_spec.rb new file mode 100644 index 000000000..8d763ff9d --- /dev/null +++ b/cinder/spec/defines/cinder_type_spec.rb @@ -0,0 +1,32 @@ +#Author: Andrew Woodward + +require 'spec_helper' + +describe 'cinder::type' do + + let(:title) {'hippo'} + + let :params do { + :set_value => ['name1','name2'], + :set_key => 'volume_backend_name', + :os_password => 'asdf', + :os_tenant_name => 'admin', + :os_username => 'admin', + :os_auth_url => 'http://127.127.127.1:5000/v2.0/', + } + end + + it 'should have its execs' do + should contain_exec('cinder type-create hippo').with( + :command => 'cinder type-create hippo', + :environment => [ + 'OS_TENANT_NAME=admin', + 'OS_USERNAME=admin', + 'OS_PASSWORD=asdf', + 'OS_AUTH_URL=http://127.127.127.1:5000/v2.0/'], + :unless => 'cinder type-list | grep hippo', + :require => 'Package[python-cinderclient]') + should contain_exec('cinder type-key hippo set volume_backend_name=name1') + should contain_exec('cinder type-key hippo set volume_backend_name=name2') + end +end diff --git a/cinder/spec/shared_examples.rb b/cinder/spec/shared_examples.rb new file mode 100644 index 000000000..d92156a36 --- /dev/null +++ b/cinder/spec/shared_examples.rb @@ -0,0 +1,5 @@ +shared_examples_for "a Puppet::Error" do |description| + it "with message matching #{description.inspect}" do + expect { should have_class_count(1) }.to raise_error(Puppet::Error, description) + end +end diff --git a/cinder/spec/spec_helper.rb b/cinder/spec/spec_helper.rb new file mode 100644 index 000000000..53d4dd02d --- /dev/null +++ b/cinder/spec/spec_helper.rb @@ -0,0 +1,7 @@ +require 'puppetlabs_spec_helper/module_spec_helper' +require 'shared_examples' + +RSpec.configure do |c| + c.alias_it_should_behave_like_to :it_configures, 'configures' + c.alias_it_should_behave_like_to :it_raises, 'raises' +end diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 000000000..2085807d2 --- /dev/null +++ b/common/.gitignore @@ -0,0 +1,3 @@ +old/ +rpmbuild/ + diff --git a/common/COPYING b/common/COPYING new file mode 100644 index 000000000..dba13ed2d --- /dev/null +++ b/common/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/common/COPYRIGHT b/common/COPYRIGHT new file mode 100644 index 000000000..480149fb7 --- /dev/null +++ b/common/COPYRIGHT @@ -0,0 +1,16 @@ +Copyright (C) 2012-2013+ James Shubin +Written by James Shubin + +This puppet module is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This puppet module 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + diff --git a/common/INSTALL b/common/INSTALL new file mode 100644 index 000000000..f497841f7 --- /dev/null +++ b/common/INSTALL @@ -0,0 +1,18 @@ +To install this puppet module, copy this folder to your puppet modulepath. + +You can usually find out where this is by running: + +$ puppet config print modulepath + +on your puppetmaster. In my case, this contains the directory: + +/etc/puppet/modules/ + +I keep all of my puppet modules in git managed directories named: + +puppet- + +You must remove the 'puppet-' prefix from the directory name for it to work! + +Happy hacking! + diff --git a/common/Makefile b/common/Makefile new file mode 100644 index 000000000..5ff251842 --- /dev/null +++ b/common/Makefile @@ -0,0 +1,146 @@ +# Common puppet utilities module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +.PHONY: all docs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms +.SILENT: + +# version of the program +VERSION := $(shell cat VERSION) +RELEASE = 1 +SPEC = rpmbuild/SPECS/puppet-common.spec +SOURCE = rpmbuild/SOURCES/puppet-common-$(VERSION).tar.bz2 +SRPM = rpmbuild/SRPMS/puppet-common-$(VERSION)-$(RELEASE).src.rpm +RPM = rpmbuild/RPMS/puppet-common-$(VERSION)-$(RELEASE).rpm +SERVER = 'download.gluster.org' +REMOTE_PATH = 'purpleidea/puppet-common' + +all: docs rpm + +docs: puppet-common-documentation.pdf + +puppet-common-documentation.pdf: DOCUMENTATION.md + pandoc DOCUMENTATION.md -o 'puppet-common-documentation.pdf' + +# +# aliases +# +# TODO: does making an rpm depend on making a .srpm first ? +rpm: $(SRPM) $(RPM) + # do nothing + +srpm: $(SRPM) + # do nothing + +spec: $(SPEC) + # do nothing + +tar: $(SOURCE) + # do nothing + +upload: upload-sources upload-srpms upload-rpms + # do nothing + +# +# rpmbuild +# +$(RPM): $(SPEC) $(SOURCE) + @echo Running rpmbuild -bb... + rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bb $(SPEC) && \ + mv rpmbuild/RPMS/noarch/puppet-common-$(VERSION)-$(RELEASE).*.rpm $(RPM) + +$(SRPM): $(SPEC) $(SOURCE) + @echo Running rpmbuild -bs... + rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bs $(SPEC) + # renaming is not needed because we aren't using the dist variable + #mv rpmbuild/SRPMS/puppet-common-$(VERSION)-$(RELEASE).*.src.rpm $(SRPM) + +# +# spec +# +$(SPEC): rpmbuild/ puppet-common.spec.in + @echo Running templater... + #cat puppet-common.spec.in > $(SPEC) + sed -e s/__VERSION__/$(VERSION)/ -e s/__RELEASE__/$(RELEASE)/ < puppet-common.spec.in > $(SPEC) + # append a changelog to the .spec file + git log --format="* %cd %aN <%aE>%n- (%h) %s%d%n" --date=local | sed -r 's/[0-9]+:[0-9]+:[0-9]+ //' >> $(SPEC) + +# +# archive +# +$(SOURCE): rpmbuild/ + @echo Running git archive... + # use HEAD if tag doesn't exist yet, so that development is easier... + git archive --prefix=puppet-common-$(VERSION)/ -o $(SOURCE) $(VERSION) 2> /dev/null || (echo 'Warning: $(VERSION) does not exist.' && git archive --prefix=puppet-common-$(VERSION)/ -o $(SOURCE) HEAD) + +# TODO: ensure that each sub directory exists +rpmbuild/: + mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} + +# +# sha256sum +# +rpmbuild/SOURCES/SHA256SUMS: rpmbuild/SOURCES/ $(SOURCE) + @echo Running SOURCES sha256sum... + cd rpmbuild/SOURCES/ && sha256sum *.tar.bz2 > SHA256SUMS; cd - + +rpmbuild/SRPMS/SHA256SUMS: rpmbuild/SRPMS/ $(SRPM) + @echo Running SRPMS sha256sum... + cd rpmbuild/SRPMS/ && sha256sum *src.rpm > SHA256SUMS; cd - + +rpmbuild/RPMS/SHA256SUMS: rpmbuild/RPMS/ $(RPM) + @echo Running RPMS sha256sum... + cd rpmbuild/RPMS/ && sha256sum *.rpm > SHA256SUMS; cd - + +# +# gpg +# +rpmbuild/SOURCES/SHA256SUMS.asc: rpmbuild/SOURCES/SHA256SUMS + @echo Running SOURCES gpg... + # the --yes forces an overwrite of the SHA256SUMS.asc if necessary + gpg2 --yes --clearsign rpmbuild/SOURCES/SHA256SUMS + +rpmbuild/SRPMS/SHA256SUMS.asc: rpmbuild/SRPMS/SHA256SUMS + @echo Running SRPMS gpg... + gpg2 --yes --clearsign rpmbuild/SRPMS/SHA256SUMS + +rpmbuild/RPMS/SHA256SUMS.asc: rpmbuild/RPMS/SHA256SUMS + @echo Running RPMS gpg... + gpg2 --yes --clearsign rpmbuild/RPMS/SHA256SUMS + +# +# upload +# +# upload to public server +upload-sources: rpmbuild/SOURCES/ rpmbuild/SOURCES/SHA256SUMS rpmbuild/SOURCES/SHA256SUMS.asc + if [ "`cat rpmbuild/SOURCES/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/SOURCES/ && cat SHA256SUMS'`" ]; then \ + echo Running SOURCES upload...; \ + rsync -avz rpmbuild/SOURCES/ $(SERVER):$(REMOTE_PATH)/SOURCES/; \ + fi + +upload-srpms: rpmbuild/SRPMS/ rpmbuild/SRPMS/SHA256SUMS rpmbuild/SRPMS/SHA256SUMS.asc + if [ "`cat rpmbuild/SRPMS/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/SRPMS/ && cat SHA256SUMS'`" ]; then \ + echo Running SRPMS upload...; \ + rsync -avz rpmbuild/SRPMS/ $(SERVER):$(REMOTE_PATH)/SRPMS/; \ + fi + +upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.asc + if [ "`cat rpmbuild/RPMS/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/RPMS/ && cat SHA256SUMS'`" ]; then \ + echo Running RPMS upload...; \ + rsync -avz --prune-empty-dirs rpmbuild/RPMS/ $(SERVER):$(REMOTE_PATH)/RPMS/; \ + fi + +# vim: ts=8 diff --git a/common/README b/common/README new file mode 100644 index 000000000..2e03bc9ea --- /dev/null +++ b/common/README @@ -0,0 +1,22 @@ +This is puppet-common a puppet module for common things. + +Please read the INSTALL file for instructions on getting this installed. +Look in the examples/ folder for usage. If none exist, please contribute one! +This code may be a work in progress. The interfaces may change without notice. +Patches are welcome, but please be patient. They are best received by email. +Please ping me if you have big changes in mind, before you write a giant patch. + +Module specific notes: +* This module is a useful dependency for some of my other modules. +* You may also find some of the individual pieces useful. +* I have removed functionality that now exists in the puppet "stdlib". +* Some of the functionality is so useful, that this imports types directly. + +Dependencies: +* puppetlabs-stdlib (required) +* my puppet-puppet module (optional) + + +Happy hacking, +James Shubin , https://ttboj.wordpress.com/ + diff --git a/common/VERSION b/common/VERSION new file mode 100644 index 000000000..4e379d2bf --- /dev/null +++ b/common/VERSION @@ -0,0 +1 @@ +0.0.2 diff --git a/common/examples/again-delta-example.pp b/common/examples/again-delta-example.pp new file mode 100644 index 000000000..d1e73d76f --- /dev/null +++ b/common/examples/again-delta-example.pp @@ -0,0 +1,23 @@ +# this is just a proof of concept example, to help you visualize how this works + +include common::again + +# when notified, this timer will run puppet again, delta seconds after it ends! +common::again::delta { 'delta-timer': + delta => 120, # 2 minutes + #start_timer_now => true, # use this to start the countdown asap! +} + +file { '/tmp/foo': + content => "Something happened!\n", + # NOTE: that as long as you don't remove or change the /tmp/foo file, + # this will only cause a notify when the file needs changes done. This + # is what prevents this from infinite recursion, and lets puppet sleep. + notify => Common::Again::Delta['delta-timer'], # notify puppet! +} + +# always exec so that i can easily see when puppet runs... proof that it works! +exec { 'proof': + command => '/bin/date >> /tmp/puppet.again', +} + diff --git a/common/examples/again-example.pp b/common/examples/again-example.pp new file mode 100644 index 000000000..25cc6255f --- /dev/null +++ b/common/examples/again-example.pp @@ -0,0 +1,17 @@ +# this is just a proof of concept example, to help you visualize how this works + +include common::again + +file { '/tmp/foo': + content => "Something happened!\n", + # NOTE: that as long as you don't remove or change the /tmp/foo file, + # this will only cause a notify when the file needs changes done. This + # is what prevents this from infinite recursion, and lets puppet sleep. + notify => Exec['again'], # notify puppet! +} + +# always exec so that i can easily see when puppet runs... proof that it works! +exec { 'proof': + command => '/bin/date >> /tmp/puppet.again', +} + diff --git a/common/manifests/again.pp b/common/manifests/again.pp new file mode 100644 index 000000000..cd2bf7418 --- /dev/null +++ b/common/manifests/again.pp @@ -0,0 +1,86 @@ +# Run puppet again if notified to do so +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class common::again { + + include common::vardir + + #$vardir = $::common::vardir::module_vardir # with trailing slash + $vardir = regsubst($::common::vardir::module_vardir, '\/$', '') + + # store 'again' specific code in a separate directory + file { "${vardir}/again/": + ensure => directory, # make sure this is a directory + recurse => true, # don't recurse into directory + purge => true, # don't purge unmanaged files + force => true, # don't purge subdirs and links + require => File["${vardir}/"], + } + + file { "${vardir}/again/again.py": + # NOTE: this is actually templated, but no templating + # is actually being used. This gives us the option to + # pass in some variables if we decide we would like a + # way to get values in other than via command line... + # we could pass in some environ data or other data... + content => template('common/again/again.py.erb'), + owner => root, + group => root, + mode => 754, # if you're not root, you can't run it! + ensure => present, + require => File["${vardir}/again/"], + } + + # notify this command whenever you want to trigger another puppet run! + exec { 'again': + command => "${vardir}/again/again.py", + logoutput => on_failure, + refreshonly => true, # run whenever someone requests it! + require => File["${vardir}/again/again.py"], + } +} + +## NOTE: splitting this into a separate file didn't work properly in this module +#define common::again::delta( +# $delta = 0, +# # start timer counting now! (default is to start when puppet finishes!) +# $start_timer_now = false +#) { +# include common::vardir +# include common::again + +# #$vardir = $::common::vardir::module_vardir # with trailing slash +# $vardir = regsubst($::common::vardir::module_vardir, '\/$', '') + +# $valid_start_timer_now = $start_timer_now ? { +# true => '--start-timer-now', +# default => '', +# } + +# $arglist = ["--delta ${delta}", "${valid_start_timer_now}"] +# $args = join(delete($arglist, ''), ' ') + +# # notify this command whenever you want to trigger another puppet run! +# exec { "again-delta-${name}": +# command => "${vardir}/again/again.py ${args}", +# logoutput => on_failure, +# refreshonly => true, # run whenever someone requests it! +# require => File["${vardir}/again/again.py"], +# } +#} + +# vim: ts=8 diff --git a/common/manifests/again/delta.pp b/common/manifests/again/delta.pp new file mode 100644 index 000000000..2c2c27ed9 --- /dev/null +++ b/common/manifests/again/delta.pp @@ -0,0 +1,52 @@ +# Run puppet again if notified to do so +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: see ../again.pp for the delta code + +# NOTE: splitting this into a separate file didn't work properly in this module +define common::again::delta( + $delta = 0, + # start timer counting now! (default is to start when puppet finishes!) + $start_timer_now = false +) { + include common::vardir + include common::again + + #$vardir = $::common::vardir::module_vardir # with trailing slash + $vardir = regsubst($::common::vardir::module_vardir, '\/$', '') + + $valid_delta = inline_template('<%= [Fixnum, String].include?(@delta.class) ? @delta.to_i : 0 %>') + + $valid_start_timer_now = $start_timer_now ? { + true => '--start-timer-now', + default => '', + } + + $arglist = ["--delta ${valid_delta}", "${valid_start_timer_now}"] + $args = join(delete($arglist, ''), ' ') + + # notify this command whenever you want to trigger another puppet run! + exec { "again-delta-${name}": + command => "${vardir}/again/again.py ${args}", + logoutput => on_failure, + refreshonly => true, # run whenever someone requests it! + require => File["${vardir}/again/again.py"], + } +} + + +# vim: ts=8 diff --git a/common/manifests/frag/frag.pp b/common/manifests/frag/frag.pp new file mode 100644 index 000000000..3938006d9 --- /dev/null +++ b/common/manifests/frag/frag.pp @@ -0,0 +1,124 @@ +# Create a whole file (whole {}) from fragments (frag {}) +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# thanks to #puppet@freenode.net for some good implementation advice + +define whole( + $dir, # the directory to store the fragment + $owner = root, # the file {} defaults were used here + $group = root, + $mode = '644', + $backup = undef, # the default value is actually: puppet + $pattern = '', # /usr/bin/find -name is used + $frag = false, # set true to also act like a frag obj! + $ensure = present # TODO: does absent even work properly? +) { + $d = sprintf("%s/", regsubst($dir, '\/$', '')) # ensure trailing slash + $safe_d = shellquote($d) + $safe_p = shellquote($pattern) + $command = $pattern ? { + '' => "/usr/bin/find ${safe_d} -maxdepth 1 -type f -print0 | /bin/sort -z | /usr/bin/xargs -0 /bin/cat", + default => "/usr/bin/find ${safe_d} -maxdepth 1 -name ${safe_p} -type f -print0 | /bin/sort -z | /usr/bin/xargs -0 /bin/cat", + } + + # this is the parent (basename) dir of $name which is special if i frag + $frag_d = sprintf("%s/", regsubst($name, '((\/[\w.-]+)*)(\/)([\w.-]+)', '\1')) + + # the file (used to set perms and as a placeholder so it's not deleted) + file { "${name}": + ensure => $ensure, + owner => $owner, + group => $group, + mode => $mode, + backup => $backup, + checksum => 'md5', + before => $frag ? { # used when this is also a frag + true => Exec["${frag_d}"], + default => undef, + } + } + + # ensure directory exists and is managed + file { "${d}": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + before => Exec["${d}"], + } + + # actually make the file from fragments with this command + # use >| to force target file clobbering (ignore a: "set -o noclobber") + exec { "${d}": + command => "${command} >| ${name}", + # NOTE: if we don't use 'bash -c' this spurriously sends notify + # actually check that the file matches what it really should be + unless => "/bin/bash -c '/usr/bin/diff -q <(/bin/cat ${name}) <(${command})'", + logoutput => on_failure, + # if we're a frag, make sure that we build ourself out before + # the parent `whole` object which uses us, builds itself out! + before => $frag ? { + true => Exec["${frag_d}"], + default => undef, + }, + # if we're a frag, then it's important to wait for the parent + # frag collection directory to get created before i make this + require => $frag ? { + true => File["${frag_d}"], + default => undef, + }, + } +} + +# frag supports both $source and $content. if $source is not empty, it is used, +# otherwise content is used. frag should behave like a first class file object. +define frag( # dir to store frag, is path in namevar + $owner = root, # the file {} defaults were chosen here + $group = root, + $mode = '644', + $backup = undef, # the default value is actually: puppet + $ensure = present, + $content = '', + $source = '' + # TODO: add more file object features if someone needs them or if bored +) { + # finds the file name in a complete path; eg: /tmp/dir/file => file + #$x = regsubst($name, '(\/[\w.]+)*(\/)([\w.]+)', '\3') + # finds the basepath in a complete path; eg: /tmp/dir/file => /tmp/dir/ + $d = sprintf("%s/", regsubst($name, '((\/[\w.-]+)*)(\/)([\w.-]+)', '\1')) + + # the file fragment + file { "${name}": + ensure => $ensure, + owner => $owner, + group => $group, + mode => $mode, + backup => $backup, + content => $source ? { + '' => $content, + default => undef, + }, + source => $source ? { + '' => undef, + default => $source, + }, + before => Exec["${d}"], + require => File["${d}"], + } +} + +# vim: ts=8 diff --git a/common/manifests/init.pp b/common/manifests/init.pp new file mode 100644 index 000000000..fa9bec3eb --- /dev/null +++ b/common/manifests/init.pp @@ -0,0 +1,23 @@ +# Common puppet utilities module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import 'frag/*.pp' + +class common { + + +} diff --git a/common/manifests/vardir.pp b/common/manifests/vardir.pp new file mode 100644 index 000000000..f7626c94d --- /dev/null +++ b/common/manifests/vardir.pp @@ -0,0 +1,52 @@ +# Common puppet utilities module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class common::vardir { # module vardir snippet + if "${::puppet_vardirtmp}" == '' { + if "${::puppet_vardir}" == '' { + # here, we require that the puppetlabs fact exist! + fail('Fact: $puppet_vardir is missing!') + } + $tmp = sprintf("%s/tmp/", regsubst($::puppet_vardir, '\/$', '')) + # base directory where puppet modules can work and namespace in + file { "${tmp}": + ensure => directory, # make sure this is a directory + recurse => false, # don't recurse into directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, + group => nobody, + mode => 600, + backup => false, # don't backup to filebucket + #before => File["${module_vardir}"], # redundant + #require => Package['puppet'], # no puppet module seen + } + } else { + $tmp = sprintf("%s/", regsubst($::puppet_vardirtmp, '\/$', '')) + } + $module_vardir = sprintf("%s/common/", regsubst($tmp, '\/$', '')) + file { "${module_vardir}": # /var/lib/puppet/tmp/common/ + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${tmp}"], # File['/var/lib/puppet/tmp/'] + } +} + +# vim: ts=8 diff --git a/common/puppet-common.spec.in b/common/puppet-common.spec.in new file mode 100644 index 000000000..9cd21ab8a --- /dev/null +++ b/common/puppet-common.spec.in @@ -0,0 +1,40 @@ +%global puppet_module_version __VERSION__ + +Name: puppet-common +Version: __VERSION__ +#Release: __RELEASE__%{?dist} # use this to make dist specific builds +Release: __RELEASE__ +Summary: A puppet module filled with common puppet utilities +License: AGPLv3+ +URL: https://github.com/purpleidea/puppet-common +Source0: https://download.gluster.org/pub/gluster/purpleidea/puppet-common/SOURCES/puppet-common-%{puppet_module_version}.tar.bz2 +BuildArch: noarch + +Requires: puppet >= 3.0.0 +Requires: puppetlabs-stdlib >= 4.1.0 + +%description +Common puppet utilities that are useful for other puppet modules. + +%prep +%setup -c -q -T -D -a 0 + +find %{_builddir} -type f -name ".*" -exec rm {} + +find %{_builddir} -size 0 -exec rm {} + +find %{_builddir} \( -name "*.pl" -o -name "*.sh" \) -exec chmod +x {} + +find %{_builddir} \( -name "*.pp" -o -name "*.py" \) -exec chmod -x {} + +find %{_builddir} \( -name "*.rb" -o -name "*.erb" \) -exec chmod -x {} + -exec sed -i "/^#!/{d;q}" {} + + +%build + +%install +rm -rf %{buildroot} +# _datadir is typically /usr/share/ +install -d -m 0755 %{buildroot}/%{_datadir}/puppet/modules/ +cp -r puppet-common-%{puppet_module_version} %{buildroot}/%{_datadir}/puppet/modules/common + +%files +%{_datadir}/puppet/modules/* + +# this changelog is auto-generated by git log +%changelog diff --git a/common/templates/again/again.py.erb b/common/templates/again/again.py.erb new file mode 100755 index 000000000..5eb1c3125 --- /dev/null +++ b/common/templates/again/again.py.erb @@ -0,0 +1,189 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +Run puppet again if notified to do so + +This script is only executed by a puppet exec type. When run, it forks and then +waits for the parent process (puppet) to exit. It then runs puppet "again", +with the same arguments it was originally started with. + +The exec type for "again" is already provided. To activate it, you notify it: + + include again + sometype { 'foo': + notify => Exec['again'], # notify it like this + } + +This is particularly useful if you know that when one of your types runs, it +*will* need another puppet execution to finish building. Sadly, for certain +complex puppet modules, this is unavoidable. You can however make sure to avoid +infinite loops, which will just waste system resources. +""" +# Run puppet again if notified to do so +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import sys +import time +import math +import errno +import argparse + +DELAY = 0 # wait this many seconds after puppet exit before running again +LOCKFILE = '/var/lib/puppet/state/agent_catalog_run.lock' # lockfile path + +def merge(a, b): # merge two hashes and return the union + r = a.copy() + r.update(b) + return r + +def is_running(pid): + """ + os.waitpid() may not work if the process is not a child of the current + process. In that case, using os.kill(pid, 0) may indeed be the best + solution. Note that, in general, there are three likely outcomes of + calling os.kill() on a process: + + * If the process exists and belongs to you, the call succeeds. + * If the process exists but belong to another user, it throws an + OSError with the errno attribute set to errno.EPERM. + * If the process does not exist, it throws an OSError with the errno + attribute set to errno.ESRCH. + """ + try: + os.kill(pid, 0) + except OSError as e: + if e.errno == errno.ESRCH: + return False + return True + +def is_locked(): + """ + Lets us know if a puppet agent is currently running. + """ + # TODO: is there a more reliable way to do this ? This is sort of racy. + return os.path.isfile(LOCKFILE) + +parser = argparse.ArgumentParser(description="Utility used by Puppet Exec['again'].") +parser.add_argument('--delta', dest='delta', action='store', type=int, required=False, default=0) +# start delta timer immediately, instead of waiting till puppet finishes... +parser.add_argument('--start-timer-now', dest='start_timer_now', action='store_true', default=False, required=False) +args = parser.parse_args() + +start = time.time() # TODO: use time.monotonic() if exists +service = False # are we running as a service ? +pid = os.getpid() # my pid +ppid = os.getppid() # parent pid + +# parse parent cmdline +with open("/proc/%d/cmdline" % ppid, 'r') as f: + cmdline = f.read() +argv = cmdline.split("\0") # separated by nulls (the: ^@ character in vim) +if argv[-1] == '': argv.pop() # remove empty element at end of list if exists + +# parse parent environ +with open("/proc/%d/environ" % ppid, 'r') as f: + environ = f.read() + +env = environ.split("\0") +if env[-1] == '': env.pop() # as above, there is a trailing null to remove! +env = map(lambda x: {x.split('=')[0]: '='.join(x.split('=')[1:])}, env) # list! +env = reduce(lambda a,b: merge(a,b), env) # merge the hash list into hash + +# TODO: does the noop detection work when we run as a service ? (probably not!) +# if we're running as no op, then repeat execution won't help +if '--noop' in argv: + sys.exit(0) + +# TODO: do a sanity check to verify that we've got a valid puppet cmdline... + +# heuristic, based on previous experiments, that service agents have a weird $0 +# see: $0 = "puppet agent: applying configuration" in: [...]/puppet/agent.rb +if argv[0].startswith('puppet agent: '): + service = True # bonus + argv = ['/usr/bin/puppet', 'agent', '--test'] + # NOTE: create a shim, if you ever need help debugging :) + # argv.insert(0, '/tmp/shim.sh') + +# if we're running as a service, kick off a "one of" run +# TODO: is there a more reliable way to detect this ? +if not('--test' in argv) and not('-t' in argv): + service = True # this is the main setter of this variable + argv.append('--test') # TODO: this isn't ideal, but it's safe enough! + +# fork a child process. return 0 in the child and the child’s process id in the +# parent. if an error occurs OSError is raised. +try: + fork = os.fork() # TODO: forkpty() instead ? + +except OSError, e: + print >> sys.stderr, "Error forking: %s" % str(e) + sys.exit(1) + +# branch +if fork == 0: # child + # wait for ppid to exit... + # TODO: we can probably remove the service check altogether, because + # actually the ppid does spawn, and then exit i think... it doesn't + # mean we can skip the is_locked() check, but we don't have to verify + # that we're really a service, because the ppid does dissapear. + if not service: # the service pid shouldn't ever exit... + while is_running(ppid): + time.sleep(1) + + if not args.start_timer_now: + start = time.time() # TODO: use time.monotonic() if exists + + # wait for any agent runs to finish... + while is_locked(): + time.sleep(1) + + if service: # we need to wait for is_locked to end first... + if not args.start_timer_now: + start = time.time() # TODO: use time.monotonic() if exists + + # optionally delay before starting puppet again + if DELAY > 0: + time.sleep(int(DELAY)) + + # wait out the --delta timer + while True: + delta = time.time() - start # time elapsed + timeleft = args.delta - delta # time left + if int(timeleft) <= 0: + break + + time.sleep(int(math.ceil(timeleft))) # this much time left + + # wait for any agents to finish running, now that we waited for timers! + # NOTE: if puppet starts running immediately after this spinlock ended, + # but before our exec call runs, then when our exec runs puppet it will + # exit right away due to the puppet lock. this is a race condition, but + # it isn't a problem because the effect of having puppet run right away + # after this spinlock, will be successful, albeit by different means... + while is_locked(): + time.sleep(1) + + # now run puppet the same way it ran in cmdline + # NOTE: env is particularly important or puppet breaks :( + os.execvpe(argv[0], argv, env) # this command does not return! + +else: # parent + print "pid: %d will try to puppet again..." % fork + sys.exit(0) # let puppet exit successfully! + +# vim: ts=8 diff --git a/concat b/concat deleted file mode 160000 index 07bba0bca..000000000 --- a/concat +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 07bba0bcad2e3a2baf19dbff8b1a5146d9141153 diff --git a/concat/.fixtures.yml b/concat/.fixtures.yml new file mode 100644 index 000000000..67added0a --- /dev/null +++ b/concat/.fixtures.yml @@ -0,0 +1,7 @@ +fixtures: + repositories: + 'stdlib': + repo: 'git://github.com/puppetlabs/puppetlabs-stdlib.git' + ref: '4.2.0' + symlinks: + 'concat': '#{source_dir}' diff --git a/concat/.gitattributes b/concat/.gitattributes new file mode 100644 index 000000000..2e05fd47d --- /dev/null +++ b/concat/.gitattributes @@ -0,0 +1 @@ +*.sh eol=lf diff --git a/concat/.gitignore b/concat/.gitignore new file mode 100644 index 000000000..b5b7a00d6 --- /dev/null +++ b/concat/.gitignore @@ -0,0 +1,7 @@ +pkg/ +Gemfile.lock +vendor/ +spec/fixtures/ +.vagrant/ +.bundle/ +coverage/ diff --git a/concat/.sync.yml b/concat/.sync.yml new file mode 100644 index 000000000..ed97d539c --- /dev/null +++ b/concat/.sync.yml @@ -0,0 +1 @@ +--- diff --git a/concat/.travis.yml b/concat/.travis.yml new file mode 100644 index 000000000..a40ae502e --- /dev/null +++ b/concat/.travis.yml @@ -0,0 +1,17 @@ +--- +language: ruby +bundler_args: --without development +script: "bundle exec rake validate && bundle exec rake lint && bundle exec rake spec SPEC_OPTS='--format documentation'" +matrix: + fast_finish: true + include: + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.6.0" + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.7.0" + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 3.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 3.0" +notifications: + email: false diff --git a/concat/CHANGELOG b/concat/CHANGELOG new file mode 100644 index 000000000..c66b922d4 --- /dev/null +++ b/concat/CHANGELOG @@ -0,0 +1,127 @@ +2014-05-14 1.1.0 + +Summary + +This release is primarily a bugfix release since 1.1.0-rc1. + +Features: +- Improved testing, with tests moved to beaker + +Bugfixes: +- No longer attempts to set fragment owner and mode on Windows +- Fix numeric sorting +- Fix incorrect quoting +- Fix newlines + +2014-01-03 1.1.0-rc1 + +Summary: + +This release of concat was 90% written by Joshua Hoblitt, and the module team +would like to thank him for the huge amount of work he put into this release. + +This module deprecates a bunch of old parameters and usage patterns, modernizes +much of the manifest code, simplifies a whole bunch of logic and makes +improvements to almost all parts of the module. + +The other major feature is windows support, courtesy of luisfdez, with an +alternative version of the concat bash script in ruby. We've attempted to +ensure that there are no backwards incompatible changes, all users of 1.0.0 +should be able to use 1.1.0 without any failures, but you may find deprecation +warnings and we'll be aggressively moving for a 2.0 to remove those too. + +For further information on deprecations, please read: +https://github.com/puppetlabs/puppetlabs-concat/blob/master/README.md#api-deprecations + +Removed: +- Puppet 0.24 support. +- Filebucket backup of all file resources except the target concatenated file. +- Default owner/user/group values. +- Purging of long unused /usr/local/bin/concatfragments.sh + +Features: +- Windows support via a ruby version of the concat bash script. +- Huge amount of acceptance testing work added. +- Documentation (README) completely rewritten. +- New parameters in concat: + - `ensure`: Controls if the file should be present/absent at all. +- Remove requirement to include concat::setup in manifests. +- Made `gnu` parameter deprecated. +- Added parameter validation. + +Bugfixes: +- Ensure concat::setup runs before concat::fragment in all cases. +- Pluginsync references updated for modern Puppet. +- Fix incorrect group parameter. +- Use $owner instead of $id to avoid confusion with $::id +- Compatibility fixes for Puppet 2.7/ruby 1.8.7 +- Use LC_ALL=C instead of LANG=C +- Always exec the concatfragments script as root when running as root. +- Syntax and other cleanup changes. + +2013-08-09 1.0.0 + +Summary: + +Many new features and bugfixes in this release, and if you're a heavy concat +user you should test carefully before upgrading. The features should all be +backwards compatible but only light testing has been done from our side before +this release. + +Features: +- New parameters in concat: + - `replace`: specify if concat should replace existing files. + - `ensure_newline`: controls if fragments should contain a newline at the end. +- Improved README documentation. +- Add rspec:system tests (rake spec:system to test concat) + +Bugfixes +- Gracefully handle \n in a fragment resource name. +- Adding more helpful message for 'pluginsync = true' +- Allow passing `source` and `content` directly to file resource, rather than +defining resource defaults. +- Added -r flag to read so that filenames with \ will be read correctly. +- sort always uses LANG=C. +- Allow WARNMSG to contain/start with '#'. +- Replace while-read pattern with for-do in order to support Solaris. + +CHANGELOG: +- 2010/02/19 - initial release +- 2010/03/12 - add support for 0.24.8 and newer + - make the location of sort configurable + - add the ability to add shell comment based warnings to + top of files + - add the ablity to create empty files +- 2010/04/05 - fix parsing of WARN and change code style to match rest + of the code + - Better and safer boolean handling for warn and force + - Don't use hard coded paths in the shell script, set PATH + top of the script + - Use file{} to copy the result and make all fragments owned + by root. This means we can chnage the ownership/group of the + resulting file at any time. + - You can specify ensure => "/some/other/file" in concat::fragment + to include the contents of a symlink into the final file. +- 2010/04/16 - Add more cleaning of the fragment name - removing / from the $name +- 2010/05/22 - Improve documentation and show the use of ensure => +- 2010/07/14 - Add support for setting the filebucket behavior of files +- 2010/10/04 - Make the warning message configurable +- 2010/12/03 - Add flags to make concat work better on Solaris - thanks Jonathan Boyett +- 2011/02/03 - Make the shell script more portable and add a config option for root group +- 2011/06/21 - Make base dir root readable only for security +- 2011/06/23 - Set base directory using a fact instead of hardcoding it +- 2011/06/23 - Support operating as non privileged user +- 2011/06/23 - Support dash instead of bash or sh +- 2011/07/11 - Better solaris support +- 2011/12/05 - Use fully qualified variables +- 2011/12/13 - Improve Nexenta support +- 2012/04/11 - Do not use any GNU specific extensions in the shell script +- 2012/03/24 - Comply to community style guides +- 2012/05/23 - Better errors when basedir isnt set +- 2012/05/31 - Add spec tests +- 2012/07/11 - Include concat::setup in concat improving UX +- 2012/08/14 - Puppet Lint improvements +- 2012/08/30 - The target path can be different from the $name +- 2012/08/30 - More Puppet Lint cleanup +- 2012/09/04 - RELEASE 0.2.0 +- 2012/12/12 - Added (file) $replace parameter to concat diff --git a/concat/CONTRIBUTING.md b/concat/CONTRIBUTING.md new file mode 100644 index 000000000..e1288478a --- /dev/null +++ b/concat/CONTRIBUTING.md @@ -0,0 +1,234 @@ +Checklist (and a short version for the impatient) +================================================= + + * Commits: + + - Make commits of logical units. + + - Check for unnecessary whitespace with "git diff --check" before + committing. + + - Commit using Unix line endings (check the settings around "crlf" in + git-config(1)). + + - Do not check in commented out code or unneeded files. + + - The first line of the commit message should be a short + description (50 characters is the soft limit, excluding ticket + number(s)), and should skip the full stop. + + - Associate the issue in the message. The first line should include + the issue number in the form "(#XXXX) Rest of message". + + - The body should provide a meaningful commit message, which: + + - uses the imperative, present tense: "change", not "changed" or + "changes". + + - includes motivation for the change, and contrasts its + implementation with the previous behavior. + + - Make sure that you have tests for the bug you are fixing, or + feature you are adding. + + - Make sure the test suites passes after your commit: + `bundle exec rspec spec/acceptance` More information on [testing](#Testing) below + + - When introducing a new feature, make sure it is properly + documented in the README.md + + * Submission: + + * Pre-requisites: + + - Sign the [Contributor License Agreement](https://cla.puppetlabs.com/) + + - Make sure you have a [GitHub account](https://github.com/join) + + - [Create a ticket](http://projects.puppetlabs.com/projects/modules/issues/new), or [watch the ticket](http://projects.puppetlabs.com/projects/modules/issues) you are patching for. + + * Preferred method: + + - Fork the repository on GitHub. + + - Push your changes to a topic branch in your fork of the + repository. (the format ticket/1234-short_description_of_change is + usually preferred for this project). + + - Submit a pull request to the repository in the puppetlabs + organization. + +The long version +================ + + 1. Make separate commits for logically separate changes. + + Please break your commits down into logically consistent units + which include new or changed tests relevant to the rest of the + change. The goal of doing this is to make the diff easier to + read for whoever is reviewing your code. In general, the easier + your diff is to read, the more likely someone will be happy to + review it and get it into the code base. + + If you are going to refactor a piece of code, please do so as a + separate commit from your feature or bug fix changes. + + We also really appreciate changes that include tests to make + sure the bug is not re-introduced, and that the feature is not + accidentally broken. + + Describe the technical detail of the change(s). If your + description starts to get too long, that is a good sign that you + probably need to split up your commit into more finely grained + pieces. + + Commits which plainly describe the things which help + reviewers check the patch and future developers understand the + code are much more likely to be merged in with a minimum of + bike-shedding or requested changes. Ideally, the commit message + would include information, and be in a form suitable for + inclusion in the release notes for the version of Puppet that + includes them. + + Please also check that you are not introducing any trailing + whitespace or other "whitespace errors". You can do this by + running "git diff --check" on your changes before you commit. + + 2. Sign the Contributor License Agreement + + Before we can accept your changes, we do need a signed Puppet + Labs Contributor License Agreement (CLA). + + You can access the CLA via the [Contributor License Agreement link](https://cla.puppetlabs.com/) + + If you have any questions about the CLA, please feel free to + contact Puppet Labs via email at cla-submissions@puppetlabs.com. + + 3. Sending your patches + + To submit your changes via a GitHub pull request, we _highly_ + recommend that you have them on a topic branch, instead of + directly on "master". + It makes things much easier to keep track of, especially if + you decide to work on another thing before your first change + is merged in. + + GitHub has some pretty good + [general documentation](http://help.github.com/) on using + their site. They also have documentation on + [creating pull requests](http://help.github.com/send-pull-requests/). + + In general, after pushing your topic branch up to your + repository on GitHub, you can switch to the branch in the + GitHub UI and click "Pull Request" towards the top of the page + in order to open a pull request. + + + 4. Update the related GitHub issue. + + If there is a GitHub issue associated with the change you + submitted, then you should update the ticket to include the + location of your branch, along with any other commentary you + may wish to make. + +Testing +======= + +Getting Started +--------------- + +Our puppet modules provide [`Gemfile`](./Gemfile)s which can tell a ruby +package manager such as [bundler](http://bundler.io/) what Ruby packages, +or Gems, are required to build, develop, and test this software. + +Please make sure you have [bundler installed](http://bundler.io/#getting-started) +on your system, then use it to install all dependencies needed for this project, +by running + +```shell +% bundle install +Fetching gem metadata from https://rubygems.org/........ +Fetching gem metadata from https://rubygems.org/.. +Using rake (10.1.0) +Using builder (3.2.2) +-- 8><-- many more --><8 -- +Using rspec-system-puppet (2.2.0) +Using serverspec (0.6.3) +Using rspec-system-serverspec (1.0.0) +Using bundler (1.3.5) +Your bundle is complete! +Use `bundle show [gemname]` to see where a bundled gem is installed. +``` + +NOTE some systems may require you to run this command with sudo. + +If you already have those gems installed, make sure they are up-to-date: + +```shell +% bundle update +``` + +With all dependencies in place and up-to-date we can now run the tests: + +```shell +% rake spec +``` + +This will execute all the [rspec tests](http://rspec-puppet.com/) tests +under [spec/defines](./spec/defines), [spec/classes](./spec/classes), +and so on. rspec tests may have the same kind of dependencies as the +module they are testing. While the module defines in its [Modulefile](./Modulefile), +rspec tests define them in [.fixtures.yml](./fixtures.yml). + +Some puppet modules also come with [beaker](https://github.com/puppetlabs/beaker) +tests. These tests spin up a virtual machine under +[VirtualBox](https://www.virtualbox.org/)) with, controlling it with +[Vagrant](http://www.vagrantup.com/) to actually simulate scripted test +scenarios. In order to run these, you will need both of those tools +installed on your system. + +You can run them by issuing the following command + +```shell +% rake spec_clean +% rspec spec/acceptance +``` + +This will now download a pre-fabricated image configured in the [default node-set](./spec/acceptance/nodesets/default.yml), +install puppet, copy this module and install its dependencies per [spec/spec_helper_acceptance.rb](./spec/spec_helper_acceptance.rb) +and then run all the tests under [spec/acceptance](./spec/acceptance). + +Writing Tests +------------- + +XXX getting started writing tests. + +If you have commit access to the repository +=========================================== + +Even if you have commit access to the repository, you will still need to +go through the process above, and have someone else review and merge +in your changes. The rule is that all changes must be reviewed by a +developer on the project (that did not write the code) to ensure that +all changes go through a code review process. + +Having someone other than the author of the topic branch recorded as +performing the merge is the record that they performed the code +review. + + +Additional Resources +==================== + +* [Getting additional help](http://projects.puppetlabs.com/projects/puppet/wiki/Getting_Help) + +* [Writing tests](http://projects.puppetlabs.com/projects/puppet/wiki/Development_Writing_Tests) + +* [Patchwork](https://patchwork.puppetlabs.com) + +* [Contributor License Agreement](https://projects.puppetlabs.com/contributor_licenses/sign) + +* [General GitHub documentation](http://help.github.com/) + +* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) + diff --git a/concat/Gemfile b/concat/Gemfile new file mode 100644 index 000000000..e960f7c4b --- /dev/null +++ b/concat/Gemfile @@ -0,0 +1,27 @@ +source ENV['GEM_SOURCE'] || "https://rubygems.org" + +group :development, :test do + gem 'rake', :require => false + gem 'rspec-puppet', :require => false + gem 'puppetlabs_spec_helper', :require => false + gem 'serverspec', :require => false + gem 'puppet-lint', :require => false + gem 'beaker', :require => false + gem 'beaker-rspec', :require => false + gem 'pry', :require => false + gem 'simplecov', :require => false +end + +if facterversion = ENV['FACTER_GEM_VERSION'] + gem 'facter', facterversion, :require => false +else + gem 'facter', :require => false +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/concat/LICENSE b/concat/LICENSE new file mode 100644 index 000000000..6a9e9a194 --- /dev/null +++ b/concat/LICENSE @@ -0,0 +1,14 @@ + Copyright 2012 R.I.Pienaar + + 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/concat/Modulefile b/concat/Modulefile new file mode 100644 index 000000000..bc1f1994d --- /dev/null +++ b/concat/Modulefile @@ -0,0 +1,9 @@ +name 'puppetlabs-concat' +version '1.1.0' +source 'git://github.com/puppetlabs/puppetlabs-concat.git' +author 'Puppetlabs' +license 'Apache 2.0' +summary 'Concat module' +description 'Concat module' +project_page 'http://github.com/puppetlabs/puppetlabs-concat' +dependency 'puppetlabs/stdlib', '>= 4.2.0' diff --git a/concat/README.md b/concat/README.md new file mode 100644 index 000000000..b884a0ac7 --- /dev/null +++ b/concat/README.md @@ -0,0 +1,443 @@ +#Concat + +[![Build Status](https://travis-ci.org/puppetlabs/puppetlabs-concat.png?branch=master)](https://travis-ci.org/puppetlabs/puppetlabs-concat) + +####Table of Contents + +1. [Overview](#overview) +2. [Module Description - What the module does and why it is useful](#module-description) +3. [Setup - The basics of getting started with concat](#setup) + * [What concat affects](#what-concat-affects) + * [Setup requirements](#setup-requirements) + * [Beginning with concat](#beginning-with-concat) +4. [Usage - Configuration options and additional functionality](#usage) + * [API _deprecations_](#api-deprecations) +5. [Reference - An under-the-hood peek at what the module is doing and how](#reference) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) + +##Overview + +This module constructs files from multiple fragments in an ordered way. + +##Module Description + +This module lets you use many concat::fragment{} resources throughout +your modules to construct a single file at the end. It does this through +a shell (or ruby) script and a temporary holding space for the fragments. + +##Setup + +###What concat affects + +* Installs concatfragments.[sh|rb] based on platform. +* Adds a concat/ directory into Puppets `vardir`. + +###Beginning with concat + +To start using concat you need to create: + +* A concat{} resource for the final file. +* One or more concat::fragment{}'s. + +A minimal example might be: + +```puppet +concat { '/tmp/file': + ensure => present, +} + +concat::fragment { 'tmpfile': + target => '/tmp/file', + content => 'test contents', + order => '01' +} +``` + +##Usage + +Please be aware that there have been a number of [API +_deprecations_](#api-deprecations). + +If you wanted a /etc/motd file that listed all the major modules +on the machine. And that would be maintained automatically even +if you just remove the include lines for other modules you could +use code like below, a sample /etc/motd would be: + +
+Puppet modules on this server:
+
+    -- Apache
+    -- MySQL
+
+ +Local sysadmins can also append to the file by just editing /etc/motd.local +their changes will be incorporated into the puppet managed motd. + +```puppet +class motd { + $motd = '/etc/motd' + + concat { $motd: + owner => 'root', + group => 'root', + mode => '0644' + } + + concat::fragment{ 'motd_header': + target => $motd, + content => "\nPuppet modules on this server:\n\n", + order => '01' + } + + # local users on the machine can append to motd by just creating + # /etc/motd.local + concat::fragment{ 'motd_local': + target => $motd, + source => '/etc/motd.local', + order => '15' + } +} + +# used by other modules to register themselves in the motd +define motd::register($content="", $order='10') { + if $content == "" { + $body = $name + } else { + $body = $content + } + + concat::fragment{ "motd_fragment_$name": + target => '/etc/motd', + order => $order, + content => " -- $body\n" + } +} +``` + +To use this you'd then do something like: + +```puppet +class apache { + include apache::install, apache::config, apache::service + + motd::register{ 'Apache': } +} +``` + +##Reference + +###Classes + +####Public classes + +####Private classes +* `concat::setup`: Sets up the concat script/directories. + +###Parameters + +###Defines + +####concat + +#####`ensure` +Controls if the combined file is present or absent. + +######Example +- ensure => present +- ensure => absent + +#####`path` +Controls the destination of the file to create. + +######Example +- path => '/tmp/filename' + +#####`owner` +Set the owner of the combined file. + +######Example +- owner => 'root' + +#####`group` +Set the group of the combined file. + +######Example +- group => 'root' + +#####`mode` +Set the mode of the combined file. + +######Example +- mode => '0644' + +#####`warn` +Determine if a warning message should be added at the top of the file to let +users know it was autogenerated by Puppet. + +######Example +- warn => true +- warn => false + +#####`warn_message` +Set the contents of the warning message. + +######Example +- warn_message => 'This file is autogenerated!' + +#####`force` +Determine if empty files are allowed when no fragments were added. + +######Example +- force => true +- force => false + +#####`backup` +Controls the filebucket behavior used for the file. + +######Example +- backup => 'puppet' + +#####`replace` +Controls if Puppet should replace the destination file if it already exists. + +######Example +- replace => true +- replace => false + +#####`order` +Controls the way in which the shell script chooses to sort the files. It's +rare you'll need to adjust this. + +######Allowed Values +- order => 'alpha' +- order => 'numeric' + +#####`ensure_newline` +Ensure there's a newline at the end of the fragments. + +######Example +- ensure_newline => true +- ensure_newline => false + +####concat::fragment + +#####`target` +Choose the destination file of the fragment. + +######Example +- target => '/tmp/testfile' + +#####`content` +Create the content of the fragment. + +######Example +- content => 'test file contents' + +#####`source` +Find the sources within Puppet of the fragment. + +######Example +- source => 'puppet:///modules/test/testfile' +- source => ['puppet:///modules/test/1', 'puppet:///modules/test/2'] + +#####`order` +Order the fragments. + +######Example +- order => '01' + +Best practice is to pass a string to this parameter but integer values are accepted. + +#####`ensure` +Control the file of fragment created. + +######Example +- ensure => 'present' +- ensure => 'absent' +- ensure => 'file' +- ensure => 'directory' + +#####`mode` +Set the mode of the fragment. + +######Example +- mode => '0644' + +#####`owner` +Set the owner of the fragment. + +######Example +- owner => 'root' + +#####`group` +Set the group of the fragment. + +######Example +- group => 'root' + +#####`backup` +Control the filebucket behavior for the fragment. + +######Example +- backup => 'puppet' + +### API _deprecations_ + +#### Since version `1.0.0` + +##### `concat{}` `warn` parameter + +```puppet +concat { '/tmp/file': + ensure => present, + warn => 'true', # generates stringified boolean value warning +} +``` + +Using stringified Boolean values as the `warn` parameter to `concat` is +deprecated, generates a catalog compile time warning, and will be silently +treated as the concatenated file header/warning message in a future release. + +The following strings are considered a stringified Boolean value: + + * `'true'` + * `'yes'` + * `'on'` + * `'false'` + * `'no'` + * `'off'` + +Please migrate to using the Puppet DSL's native [Boolean data +type](http://docs.puppetlabs.com/puppet/3/reference/lang_datatypes.html#booleans). + +##### `concat{}` `gnu` parameter + +```puppet +concat { '/tmp/file': + ensure => present, + gnu => $foo, # generates deprecation warning +} +``` + +The `gnu` parameter to `concat` is deprecated, generates a catalog compile time +warning, and has no effect. This parameter will be removed in a future +release. + +Note that this parameter was silently ignored in the `1.0.0` release. + +##### `concat::fragment{}` `ensure` parameter + +```puppet +concat::fragment { 'cpuinfo': + ensure => '/proc/cpuinfo', # generates deprecation warning + target => '/tmp/file', +} +``` + +Passing a value other than `'present'` or `'absent'` as the `ensure` parameter +to `concat::fragment` is deprecated and generates a catalog compile time +warning. The warning will become a catalog compilation failure in a future +release. + +This type emulates the Puppet core `file` type's disfavored [`ensure` +semantics](http://docs.puppetlabs.com/references/latest/type.html#file-attribute-ensure) +of treating a file path as a directive to create a symlink. This feature is +problematic in several ways. It copies an API semantic of another type that is +both frowned upon and not generally well known. It's behavior may be +surprising in that the target concatenated file will not be a symlink nor is +there any common file system that has a concept of a section of a plain file +being symbolically linked to another file. Additionally, the behavior is +generally inconsistent with most Puppet types in that a missing source file +will be silently ignored. + +If you want to use the content of a file as a fragment please use the `source` +parameter. + +##### `concat::fragment{}` `mode/owner/group` parameters + +```puppet +concat::fragment { 'foo': + target => '/tmp/file', + content => 'foo', + mode => $mode, # generates deprecation warning + owner => $owner, # generates deprecation warning + group => $group, # generates deprecation warning +} +``` + +The `mode` parameter to `concat::fragment` is deprecated, generates a catalog compile time warning, and has no effect. + +The `owner` parameter to `concat::fragment` is deprecated, generates a catalog +compile time warning, and has no effect. + +The `group` parameter to `concat::fragment` is deprecated, generates a catalog +compile time warning, and has no effect. + +These parameters had no user visible effect in version `1.0.0` and will be +removed in a future release. + +##### `concat::fragment{}` `backup` parameter + +```puppet +concat::fragment { 'foo': + target => '/tmp/file', + content => 'foo', + backup => 'bar', # generates deprecation warning +} +``` + +The `backup` parameter to `concat::fragment` is deprecated, generates a catalog +compile time warning, and has no effect. It will be removed in a future +release. + +In the `1.0.0` release this parameter controlled file bucketing of the file +fragment. Bucketting the fragment(s) is redundant with bucketting the final +concatenated file and this feature has been removed. + +##### `class { 'concat::setup': }` + +```puppet +include concat::setup # generates deprecation warning + +class { 'concat::setup': } # generates deprecation warning +``` + +The `concat::setup` class is deprecated as a public API of this module and +should no longer be directly included in the manifest. This class may be +removed in a future release. + +##### Parameter validation + +While not an API depreciation, users should be aware that all public parameters +in this module are now validated for at least variable type. This may cause +validation errors in a manifest that was previously silently misbehaving. + +##Limitations + +This module has been tested on: + +* RedHat Enterprise Linux (and Centos) 5/6 +* Debian 6/7 +* Ubuntu 12.04 + +Testing on other platforms has been light and cannot be guaranteed. + +#Development + +Puppet Labs modules on the Puppet Forge are open projects, and community +contributions are essential for keeping them great. We can’t access the +huge number of platforms and myriad of hardware, software, and deployment +configurations that Puppet is intended to serve. + +We want to keep it as easy as possible to contribute changes so that our +modules work in your environment. There are a few guidelines that we need +contributors to follow so that we can have a chance of keeping on top of things. + +You can read the complete module contribution guide [on the Puppet Labs wiki.](http://projects.puppetlabs.com/projects/module-site/wiki/Module_contributing) + +###Contributors + +The list of contributors can be found at: + +https://github.com/puppetlabs/puppetlabs-concat/graphs/contributors diff --git a/concat/Rakefile b/concat/Rakefile new file mode 100644 index 000000000..5868545f2 --- /dev/null +++ b/concat/Rakefile @@ -0,0 +1,10 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_inherits_from_params_class') +PuppetLint.configuration.send('disable_class_parameter_defaults') +PuppetLint.configuration.send('disable_documentation') +PuppetLint.configuration.send('disable_single_quote_string_with_variables') +PuppetLint.configuration.ignore_paths = ["spec/**/*.pp", "pkg/**/*.pp"] diff --git a/concat/files/concatfragments.rb b/concat/files/concatfragments.rb new file mode 100755 index 000000000..cb66b03da --- /dev/null +++ b/concat/files/concatfragments.rb @@ -0,0 +1,138 @@ +#!/usr/bin/env ruby +# Script to concat files to a config file. +# +# Given a directory like this: +# /path/to/conf.d +# |-- fragments +# | |-- 00_named.conf +# | |-- 10_domain.net +# | `-- zz_footer +# +# The script supports a test option that will build the concat file to a temp location and +# use /usr/bin/cmp to verify if it should be run or not. This would result in the concat happening +# twice on each run but gives you the option to have an unless option in your execs to inhibit rebuilds. +# +# Without the test option and the unless combo your services that depend on the final file would end up +# restarting on each run, or in other manifest models some changes might get missed. +# +# OPTIONS: +# -o The file to create from the sources +# -d The directory where the fragments are kept +# -t Test to find out if a build is needed, basically concats the files to a temp +# location and compare with what's in the final location, return codes are designed +# for use with unless on an exec resource +# -w Add a shell style comment at the top of the created file to warn users that it +# is generated by puppet +# -f Enables the creation of empty output files when no fragments are found +# -n Sort the output numerically rather than the default alpha sort +# +# the command: +# +# concatfragments.rb -o /path/to/conffile.cfg -d /path/to/conf.d +# +# creates /path/to/conf.d/fragments.concat and copies the resulting +# file to /path/to/conffile.cfg. The files will be sorted alphabetically +# pass the -n switch to sort numerically. +# +# The script does error checking on the various dirs and files to make +# sure things don't fail. +require 'optparse' +require 'fileutils' + +settings = { + :outfile => "", + :workdir => "", + :test => false, + :force => false, + :warn => "", + :sortarg => "" +} + +OptionParser.new do |opts| + opts.banner = "Usage: #{$0} [options]" + opts.separator "Specific options:" + + opts.on("-o", "--outfile OUTFILE", String, "The file to create from the sources") do |o| + settings[:outfile] = o + end + + opts.on("-d", "--workdir WORKDIR", String, "The directory where the fragments are kept") do |d| + settings[:workdir] = d + end + + opts.on("-t", "--test", "Test to find out if a build is needed") do + settings[:test] = true + end + + opts.separator "Other options:" + opts.on("-w", "--warn WARNMSG", String, + "Add a shell style comment at the top of the created file to warn users that it is generated by puppet") do |w| + settings[:warn] = w + end + + opts.on("-f", "--force", "Enables the creation of empty output files when no fragments are found") do + settings[:force] = true + end + + opts.on("-n", "--sort", "Sort the output numerically rather than the default alpha sort") do + settings[:sortarg] = "-n" + end +end.parse! + +# do we have -o? +raise 'Please specify an output file with -o' unless !settings[:outfile].empty? + +# do we have -d? +raise 'Please specify fragments directory with -d' unless !settings[:workdir].empty? + +# can we write to -o? +if File.file?(settings[:outfile]) + if !File.writable?(settings[:outfile]) + raise "Cannot write to #{settings[:outfile]}" + end +else + if !File.writable?(File.dirname(settings[:outfile])) + raise "Cannot write to dirname #{File.dirname(settings[:outfile])} to create #{settings[:outfile]}" + end +end + +# do we have a fragments subdir inside the work dir? +if !File.directory?(File.join(settings[:workdir], "fragments")) && !File.executable?(File.join(settings[:workdir], "fragments")) + raise "Cannot access the fragments directory" +end + +# are there actually any fragments? +if (Dir.entries(File.join(settings[:workdir], "fragments")) - %w{ . .. }).empty? + if !settings[:force] + raise "The fragments directory is empty, cowardly refusing to make empty config files" + end +end + +Dir.chdir(settings[:workdir]) + +if settings[:warn].empty? + File.open("fragments.concat", 'w') {|f| f.write("") } +else + File.open("fragments.concat", 'w') {|f| f.write("#{settings[:warn]}\n") } +end + +# find all the files in the fragments directory, sort them numerically and concat to fragments.concat in the working dir +open('fragments.concat', 'a') do |f| + Dir.entries("fragments").sort.each{ |entry| + if File.file?(File.join("fragments", entry)) + f << File.read(File.join("fragments", entry)) + end + } +end + +if !settings[:test] + # This is a real run, copy the file to outfile + FileUtils.cp 'fragments.concat', settings[:outfile] +else + # Just compare the result to outfile to help the exec decide + if FileUtils.cmp 'fragments.concat', settings[:outfile] + exit 0 + else + exit 1 + end +end diff --git a/concat/files/concatfragments.sh b/concat/files/concatfragments.sh new file mode 100755 index 000000000..7e6b0f5c5 --- /dev/null +++ b/concat/files/concatfragments.sh @@ -0,0 +1,140 @@ +#!/bin/sh + +# Script to concat files to a config file. +# +# Given a directory like this: +# /path/to/conf.d +# |-- fragments +# | |-- 00_named.conf +# | |-- 10_domain.net +# | `-- zz_footer +# +# The script supports a test option that will build the concat file to a temp location and +# use /usr/bin/cmp to verify if it should be run or not. This would result in the concat happening +# twice on each run but gives you the option to have an unless option in your execs to inhibit rebuilds. +# +# Without the test option and the unless combo your services that depend on the final file would end up +# restarting on each run, or in other manifest models some changes might get missed. +# +# OPTIONS: +# -o The file to create from the sources +# -d The directory where the fragments are kept +# -t Test to find out if a build is needed, basically concats the files to a temp +# location and compare with what's in the final location, return codes are designed +# for use with unless on an exec resource +# -w Add a shell style comment at the top of the created file to warn users that it +# is generated by puppet +# -f Enables the creation of empty output files when no fragments are found +# -n Sort the output numerically rather than the default alpha sort +# +# the command: +# +# concatfragments.sh -o /path/to/conffile.cfg -d /path/to/conf.d +# +# creates /path/to/conf.d/fragments.concat and copies the resulting +# file to /path/to/conffile.cfg. The files will be sorted alphabetically +# pass the -n switch to sort numerically. +# +# The script does error checking on the various dirs and files to make +# sure things don't fail. + +OUTFILE="" +WORKDIR="" +TEST="" +FORCE="" +WARN="" +SORTARG="" +ENSURE_NEWLINE="" + +PATH=/sbin:/usr/sbin:/bin:/usr/bin + +## Well, if there's ever a bad way to do things, Nexenta has it. +## http://nexenta.org/projects/site/wiki/Personalities +unset SUN_PERSONALITY + +while getopts "o:s:d:tnw:fl" options; do + case $options in + o ) OUTFILE=$OPTARG;; + d ) WORKDIR=$OPTARG;; + n ) SORTARG="-n";; + w ) WARNMSG="$OPTARG";; + f ) FORCE="true";; + t ) TEST="true";; + l ) ENSURE_NEWLINE="true";; + * ) echo "Specify output file with -o and fragments directory with -d" + exit 1;; + esac +done + +# do we have -o? +if [ "x${OUTFILE}" = "x" ]; then + echo "Please specify an output file with -o" + exit 1 +fi + +# do we have -d? +if [ "x${WORKDIR}" = "x" ]; then + echo "Please fragments directory with -d" + exit 1 +fi + +# can we write to -o? +if [ -f "${OUTFILE}" ]; then + if [ ! -w "${OUTFILE}" ]; then + echo "Cannot write to ${OUTFILE}" + exit 1 + fi +else + if [ ! -w `dirname "${OUTFILE}"` ]; then + echo "Cannot write to `dirname \"${OUTFILE}\"` to create ${OUTFILE}" + exit 1 + fi +fi + +# do we have a fragments subdir inside the work dir? +if [ ! -d "${WORKDIR}/fragments" ] && [ ! -x "${WORKDIR}/fragments" ]; then + echo "Cannot access the fragments directory" + exit 1 +fi + +# are there actually any fragments? +if [ ! "$(ls -A """${WORKDIR}/fragments""")" ]; then + if [ "x${FORCE}" = "x" ]; then + echo "The fragments directory is empty, cowardly refusing to make empty config files" + exit 1 + fi +fi + +cd "${WORKDIR}" + +if [ "x${WARNMSG}" = "x" ]; then + : > "fragments.concat" +else + printf '%s\n' "$WARNMSG" > "fragments.concat" +fi + +# find all the files in the fragments directory, sort them numerically and concat to fragments.concat in the working dir +IFS_BACKUP=$IFS +IFS=' +' +for fragfile in `find fragments/ -type f -follow -print0 | xargs -0 -n1 basename | LC_ALL=C sort ${SORTARG}` +do + cat "fragments/$fragfile" >> "fragments.concat" + # Handle newlines. + if [ "x${ENSURE_NEWLINE}" != "x" ]; then + echo >> "fragments.concat" + fi +done +IFS=$IFS_BACKUP + +if [ "x${TEST}" = "x" ]; then + # This is a real run, copy the file to outfile + cp fragments.concat "${OUTFILE}" + RETVAL=$? +else + # Just compare the result to outfile to help the exec decide + cmp "${OUTFILE}" fragments.concat + RETVAL=$? +fi + +exit $RETVAL diff --git a/concat/lib/facter/concat_basedir.rb b/concat/lib/facter/concat_basedir.rb new file mode 100644 index 000000000..bfac07102 --- /dev/null +++ b/concat/lib/facter/concat_basedir.rb @@ -0,0 +1,11 @@ +# == Fact: concat_basedir +# +# A custom fact that sets the default location for fragments +# +# "${::vardir}/concat/" +# +Facter.add("concat_basedir") do + setcode do + File.join(Puppet[:vardir],"concat") + end +end diff --git a/concat/manifests/fragment.pp b/concat/manifests/fragment.pp new file mode 100644 index 000000000..28f825197 --- /dev/null +++ b/concat/manifests/fragment.pp @@ -0,0 +1,123 @@ +# == Define: concat::fragment +# +# Puts a file fragment into a directory previous setup using concat +# +# === Options: +# +# [*target*] +# The file that these fragments belong to +# [*content*] +# If present puts the content into the file +# [*source*] +# If content was not specified, use the source +# [*order*] +# By default all files gets a 10_ prefix in the directory you can set it to +# anything else using this to influence the order of the content in the file +# [*ensure*] +# Present/Absent or destination to a file to include another file +# [*mode*] +# Deprecated +# [*owner*] +# Deprecated +# [*group*] +# Deprecated +# [*backup*] +# Deprecated +# +define concat::fragment( + $target, + $content = undef, + $source = undef, + $order = '10', + $ensure = undef, + $mode = undef, + $owner = undef, + $group = undef, + $backup = undef +) { + validate_string($target) + validate_string($content) + if !(is_string($source) or is_array($source)) { + fail('$source is not a string or an Array.') + } + if !(is_string($order) or is_integer($order)) { + fail('$order is not a string or integer.') + } + if $mode { + warning('The $mode parameter to concat::fragment is deprecated and has no effect') + } + if $owner { + warning('The $owner parameter to concat::fragment is deprecated and has no effect') + } + if $group { + warning('The $group parameter to concat::fragment is deprecated and has no effect') + } + if $backup { + warning('The $backup parameter to concat::fragment is deprecated and has no effect') + } + if $ensure == undef { + $my_ensure = getparam(Concat[$target], 'ensure') + } else { + if ! ($ensure in [ 'present', 'absent' ]) { + warning('Passing a value other than \'present\' or \'absent\' as the $ensure parameter to concat::fragment is deprecated. If you want to use the content of a file as a fragment please use the $source parameter.') + } + $my_ensure = $ensure + } + + include concat::setup + + $safe_name = regsubst($name, '[/:\n]', '_', 'GM') + $safe_target_name = regsubst($target, '[/:\n]', '_', 'GM') + $concatdir = $concat::setup::concatdir + $fragdir = "${concatdir}/${safe_target_name}" + $fragowner = $concat::setup::fragment_owner + $fragmode = $concat::setup::fragment_mode + + # The file type's semantics are problematic in that ensure => present will + # not over write a pre-existing symlink. We are attempting to provide + # backwards compatiblity with previous concat::fragment versions that + # supported the file type's ensure => /target syntax + + # be paranoid and only allow the fragment's file resource's ensure param to + # be file, absent, or a file target + $safe_ensure = $my_ensure ? { + '' => 'file', + undef => 'file', + 'file' => 'file', + 'present' => 'file', + 'absent' => 'absent', + default => $my_ensure, + } + + # if it looks line ensure => /target syntax was used, fish that out + if ! ($my_ensure in ['', 'present', 'absent', 'file' ]) { + $ensure_target = $my_ensure + } else { + $ensure_target = undef + } + + # the file type's semantics only allows one of: ensure => /target, content, + # or source + if ($ensure_target and $source) or + ($ensure_target and $content) or + ($source and $content) { + fail('You cannot specify more than one of $content, $source, $ensure => /target') + } + + if ! ($content or $source or $ensure_target) { + crit('No content, source or symlink specified') + } + + # punt on group ownership until some point in the distant future when $::gid + # can be relied on to be present + file { "${fragdir}/fragments/${order}_${safe_name}": + ensure => $safe_ensure, + owner => $fragowner, + mode => $fragmode, + source => $source, + content => $content, + backup => false, + alias => "concat_fragment_${name}", + notify => Exec["concat_${target}"] + } +} diff --git a/concat/manifests/init.pp b/concat/manifests/init.pp new file mode 100644 index 000000000..91d82ebd3 --- /dev/null +++ b/concat/manifests/init.pp @@ -0,0 +1,232 @@ +# == Define: concat +# +# Sets up so that you can use fragments to build a final config file, +# +# === Options: +# +# [*ensure*] +# Present/Absent +# [*path*] +# The path to the final file. Use this in case you want to differentiate +# between the name of a resource and the file path. Note: Use the name you +# provided in the target of your fragments. +# [*owner*] +# Who will own the file +# [*group*] +# Who will own the file +# [*mode*] +# The mode of the final file +# [*force*] +# Enables creating empty files if no fragments are present +# [*warn*] +# Adds a normal shell style comment top of the file indicating that it is +# built by puppet +# [*force*] +# [*backup*] +# Controls the filebucketing behavior of the final file and see File type +# reference for its use. Defaults to 'puppet' +# [*replace*] +# Whether to replace a file that already exists on the local system +# [*order*] +# [*ensure_newline*] +# [*gnu*] +# Deprecated +# +# === Actions: +# * Creates fragment directories if it didn't exist already +# * Executes the concatfragments.sh script to build the final file, this +# script will create directory/fragments.concat. Execution happens only +# when: +# * The directory changes +# * fragments.concat != final destination, this means rebuilds will happen +# whenever someone changes or deletes the final file. Checking is done +# using /usr/bin/cmp. +# * The Exec gets notified by something else - like the concat::fragment +# define +# * Copies the file over to the final destination using a file resource +# +# === Aliases: +# +# * The exec can notified using Exec["concat_/path/to/file"] or +# Exec["concat_/path/to/directory"] +# * The final file can be referenced as File["/path/to/file"] or +# File["concat_/path/to/file"] +# +define concat( + $ensure = 'present', + $path = $name, + $owner = undef, + $group = undef, + $mode = '0644', + $warn = false, + $force = false, + $backup = 'puppet', + $replace = true, + $order = 'alpha', + $ensure_newline = false, + $gnu = undef +) { + validate_re($ensure, '^present$|^absent$') + validate_absolute_path($path) + validate_string($owner) + validate_string($group) + validate_string($mode) + if ! (is_string($warn) or $warn == true or $warn == false) { + fail('$warn is not a string or boolean') + } + validate_bool($force) + validate_string($backup) + validate_bool($replace) + validate_re($order, '^alpha$|^numeric$') + validate_bool($ensure_newline) + if $gnu { + warning('The $gnu parameter to concat is deprecated and has no effect') + } + + include concat::setup + + $safe_name = regsubst($name, '[/:]', '_', 'G') + $concatdir = $concat::setup::concatdir + $fragdir = "${concatdir}/${safe_name}" + $concat_name = 'fragments.concat.out' + $script_command = $concat::setup::script_command + $default_warn_message = '# This file is managed by Puppet. DO NOT EDIT.' + $bool_warn_message = 'Using stringified boolean values (\'true\', \'yes\', \'on\', \'false\', \'no\', \'off\') to represent boolean true/false as the $warn parameter to concat is deprecated and will be treated as the warning message in a future release' + + case $warn { + true: { + $warn_message = $default_warn_message + } + 'true', 'yes', 'on': { + warning($bool_warn_message) + $warn_message = $default_warn_message + } + false: { + $warn_message = '' + } + 'false', 'no', 'off': { + warning($bool_warn_message) + $warn_message = '' + } + default: { + $warn_message = $warn + } + } + + $warnmsg_escaped = regsubst($warn_message, '\'', '\'\\\'\'', 'G') + $warnflag = $warnmsg_escaped ? { + '' => '', + default => "-w '${warnmsg_escaped}'" + } + + $forceflag = $force ? { + true => '-f', + false => '', + } + + $orderflag = $order ? { + 'numeric' => '-n', + 'alpha' => '', + } + + $newlineflag = $ensure_newline ? { + true => '-l', + false => '', + } + + File { + backup => false, + } + + if $ensure == 'present' { + file { $fragdir: + ensure => directory, + mode => '0750', + } + + file { "${fragdir}/fragments": + ensure => directory, + mode => '0750', + force => true, + ignore => ['.svn', '.git', '.gitignore'], + notify => Exec["concat_${name}"], + purge => true, + recurse => true, + } + + file { "${fragdir}/fragments.concat": + ensure => present, + mode => '0640', + } + + file { "${fragdir}/${concat_name}": + ensure => present, + mode => '0640', + } + + file { $name: + ensure => present, + owner => $owner, + group => $group, + mode => $mode, + replace => $replace, + path => $path, + alias => "concat_${name}", + source => "${fragdir}/${concat_name}", + backup => $backup, + } + + # remove extra whitespace from string interpolation to make testing easier + $command = strip(regsubst("${script_command} -o \"${fragdir}/${concat_name}\" -d \"${fragdir}\" ${warnflag} ${forceflag} ${orderflag} ${newlineflag}", '\s+', ' ', 'G')) + + # if puppet is running as root, this exec should also run as root to allow + # the concatfragments.sh script to potentially be installed in path that + # may not be accessible by a target non-root owner. + exec { "concat_${name}": + alias => "concat_${fragdir}", + command => $command, + notify => File[$name], + subscribe => File[$fragdir], + unless => "${command} -t", + path => $::path, + require => [ + File[$fragdir], + File["${fragdir}/fragments"], + File["${fragdir}/fragments.concat"], + ], + } + } else { + file { [ + $fragdir, + "${fragdir}/fragments", + "${fragdir}/fragments.concat", + "${fragdir}/${concat_name}" + ]: + ensure => absent, + force => true, + } + + file { $path: + ensure => absent, + backup => $backup, + } + + $absent_exec_command = $::kernel ? { + 'windows' => 'cmd.exe /c exit 0', + default => 'true', + } + + $absent_exec_path = $::kernel ? { + 'windows' => $::path, + default => '/bin:/usr/bin', + } + + exec { "concat_${name}": + alias => "concat_${fragdir}", + command => $absent_exec_command, + path => $absent_exec_path + } + } +} + +# vim:sw=2:ts=2:expandtab:textwidth=79 diff --git a/concat/manifests/setup.pp b/concat/manifests/setup.pp new file mode 100644 index 000000000..1a6af6487 --- /dev/null +++ b/concat/manifests/setup.pp @@ -0,0 +1,64 @@ +# === Class: concat::setup +# +# Sets up the concat system. This is a private class. +# +# [$concatdir] +# is where the fragments live and is set on the fact concat_basedir. +# Since puppet should always manage files in $concatdir and they should +# not be deleted ever, /tmp is not an option. +# +# It also copies out the concatfragments.{sh,rb} file to ${concatdir}/bin +# +class concat::setup { + if $caller_module_name != $module_name { + warning("${name} is deprecated as a public API of the ${module_name} module and should no longer be directly included in the manifest.") + } + + if $::concat_basedir { + $concatdir = $::concat_basedir + } else { + fail ('$concat_basedir not defined. Try running again with pluginsync=true on the [master] and/or [main] section of your node\'s \'/etc/puppet/puppet.conf\'.') + } + + # owner and mode of fragment files (on windows owner and access rights should + # be inherited from concatdir and not explicitly set to avoid problems) + $fragment_owner = $::osfamily ? { 'windows' => undef, default => $::id } + $fragment_mode = $::osfamily ? { 'windows' => undef, default => '0640' } + + # PR #174 introduced changes to the concatfragments.sh script that are + # incompatible with Solaris 10 but reportedly OK on Solaris 11. As a work + # around we are enable the .rb concat script on all Solaris versions. If + # this goes smoothly, we should move towards completely eliminating the .sh + # version. + $script_name = $::osfamily? { + /(?i:(Windows|Solaris))/ => 'concatfragments.rb', + default => 'concatfragments.sh' + } + + $script_path = "${concatdir}/bin/${script_name}" + + $script_owner = $::osfamily ? { 'windows' => undef, default => $::id } + + $script_mode = $::osfamily ? { 'windows' => undef, default => '0755' } + + $script_command = $::osfamily? { + 'windows' => "ruby.exe '${script_path}'", + default => $script_path + } + + File { + backup => false, + } + + file { $script_path: + ensure => file, + owner => $script_owner, + mode => $script_mode, + source => "puppet:///modules/concat/${script_name}", + } + + file { [ $concatdir, "${concatdir}/bin" ]: + ensure => directory, + mode => '0755', + } +} diff --git a/concat/spec/acceptance/backup_spec.rb b/concat/spec/acceptance/backup_spec.rb new file mode 100644 index 000000000..7b2858d8e --- /dev/null +++ b/concat/spec/acceptance/backup_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper_acceptance' + +describe 'concat backup parameter' do + context '=> puppet' do + before :all do + shell('rm -rf /tmp/concat') + shell('mkdir -p /tmp/concat') + shell("/bin/echo 'old contents' > /tmp/concat/file") + end + + pp = <<-EOS + concat { '/tmp/concat/file': + backup => 'puppet', + } + concat::fragment { 'new file': + target => '/tmp/concat/file', + content => 'new contents', + } + EOS + + it 'applies the manifest twice with "Filebucketed" stdout and no stderr' do + apply_manifest(pp, :catch_failures => true) do |r| + expect(r.stderr).to eq("") + expect(r.stdout).to match(/Filebucketed \/tmp\/concat\/file to puppet with sum 0140c31db86293a1a1e080ce9b91305f/) # sum is for file contents of 'old contents' + end + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain 'new contents' } + end + end + + context '=> .backup' do + before :all do + shell('rm -rf /tmp/concat') + shell('mkdir -p /tmp/concat') + shell("/bin/echo 'old contents' > /tmp/concat/file") + end + + pp = <<-EOS + concat { '/tmp/concat/file': + backup => '.backup', + } + concat::fragment { 'new file': + target => '/tmp/concat/file', + content => 'new contents', + } + EOS + + # XXX Puppet doesn't mention anything about filebucketing with a given + # extension like .backup + it 'applies the manifest twice no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain 'new contents' } + end + describe file('/tmp/concat/file.backup') do + it { should be_file } + it { should contain 'old contents' } + end + end + + # XXX The backup parameter uses validate_string() and thus can't be the + # boolean false value, but the string 'false' has the same effect in Puppet 3 + context "=> 'false'" do + before :all do + shell('rm -rf /tmp/concat') + shell('mkdir -p /tmp/concat') + shell("/bin/echo 'old contents' > /tmp/concat/file") + end + + pp = <<-EOS + concat { '/tmp/concat/file': + backup => '.backup', + } + concat::fragment { 'new file': + target => '/tmp/concat/file', + content => 'new contents', + } + EOS + + it 'applies the manifest twice with no "Filebucketed" stdout and no stderr' do + apply_manifest(pp, :catch_failures => true) do |r| + expect(r.stderr).to eq("") + expect(r.stdout).to_not match(/Filebucketed/) + end + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain 'new contents' } + end + end +end diff --git a/concat/spec/acceptance/concat_spec.rb b/concat/spec/acceptance/concat_spec.rb new file mode 100644 index 000000000..89919cc53 --- /dev/null +++ b/concat/spec/acceptance/concat_spec.rb @@ -0,0 +1,204 @@ +require 'spec_helper_acceptance' + +describe 'basic concat test' do + + shared_examples 'successfully_applied' do |pp| + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file("#{default['puppetvardir']}/concat") do + it { should be_directory } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 755 } + end + describe file("#{default['puppetvardir']}/concat/bin") do + it { should be_directory } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 755 } + end + describe file("#{default['puppetvardir']}/concat/bin/concatfragments.sh") do + it { should be_file } + it { should be_owned_by 'root' } + #it { should be_grouped_into 'root' } + it { should be_mode 755 } + end + describe file("#{default['puppetvardir']}/concat/_tmp_concat_file") do + it { should be_directory } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 750 } + end + describe file("#{default['puppetvardir']}/concat/_tmp_concat_file/fragments") do + it { should be_directory } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 750 } + end + describe file("#{default['puppetvardir']}/concat/_tmp_concat_file/fragments.concat") do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 640 } + end + describe file("#{default['puppetvardir']}/concat/_tmp_concat_file/fragments.concat.out") do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 640 } + end + end + + context 'owner/group root' do + pp = <<-EOS + concat { '/tmp/concat/file': + owner => 'root', + group => 'root', + mode => '0644', + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + order => '01', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + order => '02', + } + EOS + + it_behaves_like 'successfully_applied', pp + + describe file('/tmp/concat/file') do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 644 } + it { should contain '1' } + it { should contain '2' } + end + describe file("#{default['puppetvardir']}/concat/_tmp_concat_file/fragments/01_1") do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 640 } + end + describe file("#{default['puppetvardir']}/concat/_tmp_concat_file/fragments/02_2") do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 640 } + end + end + + context 'owner/group non-root' do + before(:all) do + shell "groupadd -g 64444 bob" + shell "useradd -u 42 -g 64444 bob" + end + after(:all) do + shell "userdel bob" + end + + pp=" + concat { '/tmp/concat/file': + owner => 'bob', + group => 'bob', + mode => '0644', + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + order => '01', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + order => '02', + } + " + + it_behaves_like 'successfully_applied', pp + + describe file('/tmp/concat/file') do + it { should be_file } + it { should be_owned_by 'bob' } + it { should be_grouped_into 'bob' } + it { should be_mode 644 } + it { should contain '1' } + it { should contain '2' } + end + describe file("#{default['puppetvardir']}/concat/_tmp_concat_file/fragments/01_1") do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 640 } + it { should contain '1' } + end + describe file("#{default['puppetvardir']}/concat/_tmp_concat_file/fragments/02_2") do + it { should be_file } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_mode 640 } + it { should contain '2' } + end + end + + context 'ensure' do + context 'works when set to present with path set' do + pp=" + concat { 'file': + ensure => present, + path => '/tmp/concat/file', + mode => '0644', + } + concat::fragment { '1': + target => 'file', + content => '1', + order => '01', + } + " + + it_behaves_like 'successfully_applied', pp + + describe file('/tmp/concat/file') do + it { should be_file } + it { should be_mode 644 } + it { should contain '1' } + end + end + context 'works when set to absent with path set' do + pp=" + concat { 'file': + ensure => absent, + path => '/tmp/concat/file', + mode => '0644', + } + concat::fragment { '1': + target => 'file', + content => '1', + order => '01', + } + " + + # Can't used shared examples as this will always trigger the exec when + # absent is set. + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should_not be_file } + end + end + end +end diff --git a/concat/spec/acceptance/deprecation_warnings_spec.rb b/concat/spec/acceptance/deprecation_warnings_spec.rb new file mode 100644 index 000000000..f139d818c --- /dev/null +++ b/concat/spec/acceptance/deprecation_warnings_spec.rb @@ -0,0 +1,230 @@ +require 'spec_helper_acceptance' + +describe 'deprecation warnings' do + + shared_examples 'has_warning'do |pp, w| + it 'applies the manifest twice with a stderr regex' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to match(/#{Regexp.escape(w)}/m) + expect(apply_manifest(pp, :catch_changes => true).stderr).to match(/#{Regexp.escape(w)}/m) + end + end + + context 'concat gnu parameter' do + pp = <<-EOS + concat { '/tmp/concat/file': + gnu => 'foo', + } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'bar', + } + EOS + w = 'The $gnu parameter to concat is deprecated and has no effect' + + it_behaves_like 'has_warning', pp, w + end + + context 'concat warn parameter =>' do + ['true', 'yes', 'on'].each do |warn| + context warn do + pp = <<-EOS + concat { '/tmp/concat/file': + warn => '#{warn}', + } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'bar', + } + EOS + w = 'Using stringified boolean values (\'true\', \'yes\', \'on\', \'false\', \'no\', \'off\') to represent boolean true/false as the $warn parameter to concat is deprecated and will be treated as the warning message in a future release' + + it_behaves_like 'has_warning', pp, w + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain '# This file is managed by Puppet. DO NOT EDIT.' } + it { should contain 'bar' } + end + end + end + + ['false', 'no', 'off'].each do |warn| + context warn do + pp = <<-EOS + concat { '/tmp/concat/file': + warn => '#{warn}', + } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'bar', + } + EOS + w = 'Using stringified boolean values (\'true\', \'yes\', \'on\', \'false\', \'no\', \'off\') to represent boolean true/false as the $warn parameter to concat is deprecated and will be treated as the warning message in a future release' + + it_behaves_like 'has_warning', pp, w + + describe file('/tmp/concat/file') do + it { should be_file } + it { should_not contain '# This file is managed by Puppet. DO NOT EDIT.' } + it { should contain 'bar' } + end + end + end + end + + context 'concat::fragment ensure parameter' do + context 'target file exists' do + before(:all) do + shell("/bin/echo 'file1 contents' > /tmp/concat/file1") + end + after(:all) do + # XXX this test may leave behind a symlink in the fragment directory + # which could cause warnings and/or breakage from the subsequent tests + # unless we clean it up. + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + shell('mkdir -p /tmp/concat') + end + + pp = <<-EOS + concat { '/tmp/concat/file': } + concat::fragment { 'foo': + target => '/tmp/concat/file', + ensure => '/tmp/concat/file1', + } + EOS + w = 'Passing a value other than \'present\' or \'absent\' as the $ensure parameter to concat::fragment is deprecated. If you want to use the content of a file as a fragment please use the $source parameter.' + + it_behaves_like 'has_warning', pp, w + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain 'file1 contents' } + end + + describe 'the fragment can be changed from a symlink to a plain file' do + pp = <<-EOS + concat { '/tmp/concat/file': } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'new content', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain 'new content' } + it { should_not contain 'file1 contents' } + end + end + end # target file exists + + context 'target does not exist' do + pp = <<-EOS + concat { '/tmp/concat/file': } + concat::fragment { 'foo': + target => '/tmp/concat/file', + ensure => '/tmp/concat/file1', + } + EOS + w = 'Passing a value other than \'present\' or \'absent\' as the $ensure parameter to concat::fragment is deprecated. If you want to use the content of a file as a fragment please use the $source parameter.' + + it_behaves_like 'has_warning', pp, w + + describe file('/tmp/concat/file') do + it { should be_file } + end + + describe 'the fragment can be changed from a symlink to a plain file' do + pp = <<-EOS + concat { '/tmp/concat/file': } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'new content', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain 'new content' } + end + end + end # target file exists + + end # concat::fragment ensure parameter + + context 'concat::fragment mode parameter' do + pp = <<-EOS + concat { '/tmp/concat/file': } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'bar', + mode => 'bar', + } + EOS + w = 'The $mode parameter to concat::fragment is deprecated and has no effect' + + it_behaves_like 'has_warning', pp, w + end + + context 'concat::fragment owner parameter' do + pp = <<-EOS + concat { '/tmp/concat/file': } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'bar', + owner => 'bar', + } + EOS + w = 'The $owner parameter to concat::fragment is deprecated and has no effect' + + it_behaves_like 'has_warning', pp, w + end + + context 'concat::fragment group parameter' do + pp = <<-EOS + concat { '/tmp/concat/file': } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'bar', + group => 'bar', + } + EOS + w = 'The $group parameter to concat::fragment is deprecated and has no effect' + + it_behaves_like 'has_warning', pp, w + end + + context 'concat::fragment backup parameter' do + pp = <<-EOS + concat { '/tmp/concat/file': } + concat::fragment { 'foo': + target => '/tmp/concat/file', + content => 'bar', + backup => 'bar', + } + EOS + w = 'The $backup parameter to concat::fragment is deprecated and has no effect' + + it_behaves_like 'has_warning', pp, w + end + + context 'include concat::setup' do + pp = <<-EOS + include concat::setup + EOS + w = 'concat::setup is deprecated as a public API of the concat module and should no longer be directly included in the manifest.' + + it_behaves_like 'has_warning', pp, w + end + +end diff --git a/concat/spec/acceptance/empty_spec.rb b/concat/spec/acceptance/empty_spec.rb new file mode 100644 index 000000000..09995282a --- /dev/null +++ b/concat/spec/acceptance/empty_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper_acceptance' + +describe 'concat force empty parameter' do + context 'should run successfully' do + pp = <<-EOS + concat { '/tmp/concat/file': + owner => root, + group => root, + mode => '0644', + force => true, + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should_not contain '1\n2' } + end + end +end diff --git a/concat/spec/acceptance/fragment_source_spec.rb b/concat/spec/acceptance/fragment_source_spec.rb new file mode 100644 index 000000000..3afd53430 --- /dev/null +++ b/concat/spec/acceptance/fragment_source_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper_acceptance' + +describe 'concat::fragment source' do + context 'should read file fragments from local system' do + before(:all) do + shell("/bin/echo 'file1 contents' > /tmp/concat/file1") + shell("/bin/echo 'file2 contents' > /tmp/concat/file2") + end + + pp = <<-EOS + concat { '/tmp/concat/foo': } + + concat::fragment { '1': + target => '/tmp/concat/foo', + source => '/tmp/concat/file1', + } + concat::fragment { '2': + target => '/tmp/concat/foo', + content => 'string1 contents', + } + concat::fragment { '3': + target => '/tmp/concat/foo', + source => '/tmp/concat/file2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/foo') do + it { should be_file } + it { should contain 'file1 contents' } + it { should contain 'string1 contents' } + it { should contain 'file2 contents' } + end + end # should read file fragments from local system + + context 'should create files containing first match only.' do + before(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + shell('mkdir -p /tmp/concat') + shell("/bin/echo 'file1 contents' > /tmp/concat/file1") + shell("/bin/echo 'file2 contents' > /tmp/concat/file2") + end + + pp = <<-EOS + concat { '/tmp/concat/result_file1': + owner => root, + group => root, + mode => '0644', + } + concat { '/tmp/concat/result_file2': + owner => root, + group => root, + mode => '0644', + } + concat { '/tmp/concat/result_file3': + owner => root, + group => root, + mode => '0644', + } + + concat::fragment { '1': + target => '/tmp/concat/result_file1', + source => [ '/tmp/concat/file1', '/tmp/concat/file2' ], + order => '01', + } + concat::fragment { '2': + target => '/tmp/concat/result_file2', + source => [ '/tmp/concat/file2', '/tmp/concat/file1' ], + order => '01', + } + concat::fragment { '3': + target => '/tmp/concat/result_file3', + source => [ '/tmp/concat/file1', '/tmp/concat/file2' ], + order => '01', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + describe file('/tmp/concat/result_file1') do + it { should be_file } + it { should contain 'file1 contents' } + it { should_not contain 'file2 contents' } + end + describe file('/tmp/concat/result_file2') do + it { should be_file } + it { should contain 'file2 contents' } + it { should_not contain 'file1 contents' } + end + describe file('/tmp/concat/result_file3') do + it { should be_file } + it { should contain 'file1 contents' } + it { should_not contain 'file2 contents' } + end + end + + context 'should fail if no match on source.' do + before(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + shell('mkdir -p /tmp/concat') + shell('/bin/rm -rf /tmp/concat/fail_no_source /tmp/concat/nofilehere /tmp/concat/nothereeither') + end + + pp = <<-EOS + concat { '/tmp/concat/fail_no_source': + owner => root, + group => root, + mode => '0644', + } + + concat::fragment { '1': + target => '/tmp/concat/fail_no_source', + source => [ '/tmp/concat/nofilehere', '/tmp/concat/nothereeither' ], + order => '01', + } + EOS + + it 'applies the manifest with resource failures' do + apply_manifest(pp, :expect_failures => true) + end + describe file('/tmp/concat/fail_no_source') do + #FIXME: Serverspec::Type::File doesn't support exists? for some reason. so... hack. + it { should_not be_file } + it { should_not be_directory } + end + end +end + diff --git a/concat/spec/acceptance/newline_spec.rb b/concat/spec/acceptance/newline_spec.rb new file mode 100644 index 000000000..1e989df2a --- /dev/null +++ b/concat/spec/acceptance/newline_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper_acceptance' + +describe 'concat ensure_newline parameter' do + context '=> false' do + pp = <<-EOS + concat { '/tmp/concat/file': + ensure_newline => false, + } + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + } + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain '12' } + end + end + + context '=> true' do + pp = <<-EOS + concat { '/tmp/concat/file': + ensure_newline => true, + } + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + } + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + #XXX ensure_newline => true causes changes on every run because the files + #are modified in place. + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain "1\n2\n" } + end + end +end diff --git a/concat/spec/acceptance/nodesets/aix-71-vcloud.yml b/concat/spec/acceptance/nodesets/aix-71-vcloud.yml new file mode 100644 index 000000000..f0ae87a5c --- /dev/null +++ b/concat/spec/acceptance/nodesets/aix-71-vcloud.yml @@ -0,0 +1,19 @@ +HOSTS: + pe-aix-71-acceptance: + roles: + - master + - dashboard + - database + - agent + - default + platform: aix-7.1-power + hypervisor: aix + ip: pe-aix-71-acceptance.delivery.puppetlabs.net +CONFIG: + type: pe + nfs_server: NONE + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/concat/spec/acceptance/nodesets/centos-59-x64.yml b/concat/spec/acceptance/nodesets/centos-59-x64.yml new file mode 100644 index 000000000..2ad90b86a --- /dev/null +++ b/concat/spec/acceptance/nodesets/centos-59-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-59-x64: + roles: + - master + platform: el-5-x86_64 + box : centos-59-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-59-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/concat/spec/acceptance/nodesets/centos-64-x64-pe.yml b/concat/spec/acceptance/nodesets/centos-64-x64-pe.yml new file mode 100644 index 000000000..7d9242f1b --- /dev/null +++ b/concat/spec/acceptance/nodesets/centos-64-x64-pe.yml @@ -0,0 +1,12 @@ +HOSTS: + centos-64-x64: + roles: + - master + - database + - dashboard + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: pe diff --git a/concat/spec/acceptance/nodesets/centos-64-x64.yml b/concat/spec/acceptance/nodesets/centos-64-x64.yml new file mode 100644 index 000000000..05540ed8c --- /dev/null +++ b/concat/spec/acceptance/nodesets/centos-64-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-64-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/concat/spec/acceptance/nodesets/centos-65-x64.yml b/concat/spec/acceptance/nodesets/centos-65-x64.yml new file mode 100644 index 000000000..4e2cb809e --- /dev/null +++ b/concat/spec/acceptance/nodesets/centos-65-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-65-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-65-x64-vbox436-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-65-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/concat/spec/acceptance/nodesets/debian-607-x64.yml b/concat/spec/acceptance/nodesets/debian-607-x64.yml new file mode 100644 index 000000000..4c8be42d0 --- /dev/null +++ b/concat/spec/acceptance/nodesets/debian-607-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-607-x64: + roles: + - master + platform: debian-6-amd64 + box : debian-607-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-607-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/concat/spec/acceptance/nodesets/debian-70rc1-x64.yml b/concat/spec/acceptance/nodesets/debian-70rc1-x64.yml new file mode 100644 index 000000000..19181c123 --- /dev/null +++ b/concat/spec/acceptance/nodesets/debian-70rc1-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-70rc1-x64: + roles: + - master + platform: debian-7-amd64 + box : debian-70rc1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-70rc1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/concat/spec/acceptance/nodesets/debian-73-x64.yml b/concat/spec/acceptance/nodesets/debian-73-x64.yml new file mode 100644 index 000000000..3e6a3a9dd --- /dev/null +++ b/concat/spec/acceptance/nodesets/debian-73-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + debian-73-x64.localhost: + roles: + - master + platform: debian-7-amd64 + box : debian-73-x64-virtualbox-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-73-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: foss diff --git a/concat/spec/acceptance/nodesets/default.yml b/concat/spec/acceptance/nodesets/default.yml new file mode 100644 index 000000000..ae812b0ae --- /dev/null +++ b/concat/spec/acceptance/nodesets/default.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-64-x64.localdomain: + roles: + - master + platform: el-6-x86_64 + box : centos-65-x64-virtualbox-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-65-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/concat/spec/acceptance/nodesets/fedora-18-x64.yml b/concat/spec/acceptance/nodesets/fedora-18-x64.yml new file mode 100644 index 000000000..136164983 --- /dev/null +++ b/concat/spec/acceptance/nodesets/fedora-18-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + fedora-18-x64: + roles: + - master + platform: fedora-18-x86_64 + box : fedora-18-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/fedora-18-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/concat/spec/acceptance/nodesets/sles-11-x64.yml b/concat/spec/acceptance/nodesets/sles-11-x64.yml new file mode 100644 index 000000000..41abe2135 --- /dev/null +++ b/concat/spec/acceptance/nodesets/sles-11-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + sles-11-x64.local: + roles: + - master + platform: sles-11-x64 + box : sles-11sp1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/sles-11sp1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/concat/spec/acceptance/nodesets/sles-11sp1-x64.yml b/concat/spec/acceptance/nodesets/sles-11sp1-x64.yml new file mode 100644 index 000000000..554c37a50 --- /dev/null +++ b/concat/spec/acceptance/nodesets/sles-11sp1-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + sles-11sp1-x64: + roles: + - master + platform: sles-11-x86_64 + box : sles-11sp1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/sles-11sp1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/concat/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml b/concat/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml new file mode 100644 index 000000000..5ca1514e4 --- /dev/null +++ b/concat/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-10044-x64: + roles: + - master + platform: ubuntu-10.04-amd64 + box : ubuntu-server-10044-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-10044-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/concat/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml b/concat/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml new file mode 100644 index 000000000..d065b304f --- /dev/null +++ b/concat/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-12042-x64: + roles: + - master + platform: ubuntu-12.04-amd64 + box : ubuntu-server-12042-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/concat/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml b/concat/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml new file mode 100644 index 000000000..cba1cd04c --- /dev/null +++ b/concat/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + ubuntu-server-1404-x64: + roles: + - master + platform: ubuntu-14.04-amd64 + box : puppetlabs/ubuntu-14.04-64-nocm + box_url : https://vagrantcloud.com/puppetlabs/ubuntu-14.04-64-nocm + hypervisor : vagrant +CONFIG: + log_level : debug + type: git diff --git a/concat/spec/acceptance/order_spec.rb b/concat/spec/acceptance/order_spec.rb new file mode 100644 index 000000000..8bcb7131c --- /dev/null +++ b/concat/spec/acceptance/order_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper_acceptance' + +describe 'concat order' do + before(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + shell('mkdir -p /tmp/concat') + end + + context '=> alpha' do + pp = <<-EOS + concat { '/tmp/concat/foo': + order => 'alpha' + } + concat::fragment { '1': + target => '/tmp/concat/foo', + content => 'string1', + } + concat::fragment { '2': + target => '/tmp/concat/foo', + content => 'string2', + } + concat::fragment { '10': + target => '/tmp/concat/foo', + content => 'string10', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/foo') do + it { should be_file } + it { should contain "string10\nstring1\nsring2" } + end + end + + context '=> numeric' do + pp = <<-EOS + concat { '/tmp/concat/foo': + order => 'numeric' + } + concat::fragment { '1': + target => '/tmp/concat/foo', + content => 'string1', + } + concat::fragment { '2': + target => '/tmp/concat/foo', + content => 'string2', + } + concat::fragment { '10': + target => '/tmp/concat/foo', + content => 'string10', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/foo') do + it { should be_file } + it { should contain "string1\nstring2\nsring10" } + end + end +end # concat order + +describe 'concat::fragment order' do + before(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + shell('mkdir -p /tmp/concat') + end + + context '=> reverse order' do + pp = <<-EOS + concat { '/tmp/concat/foo': } + concat::fragment { '1': + target => '/tmp/concat/foo', + content => 'string1', + order => '15', + } + concat::fragment { '2': + target => '/tmp/concat/foo', + content => 'string2', + # default order 10 + } + concat::fragment { '3': + target => '/tmp/concat/foo', + content => 'string3', + order => '1', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/foo') do + it { should be_file } + it { should contain "string3\nstring2\nsring1" } + end + end + + context '=> normal order' do + pp = <<-EOS + concat { '/tmp/concat/foo': } + concat::fragment { '1': + target => '/tmp/concat/foo', + content => 'string1', + order => '01', + } + concat::fragment { '2': + target => '/tmp/concat/foo', + content => 'string2', + order => '02' + } + concat::fragment { '3': + target => '/tmp/concat/foo', + content => 'string3', + order => '03', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/foo') do + it { should be_file } + it { should contain "string1\nstring2\nsring3" } + end + end +end # concat::fragment order diff --git a/concat/spec/acceptance/quoted_paths_spec.rb b/concat/spec/acceptance/quoted_paths_spec.rb new file mode 100644 index 000000000..d1a2b5f1b --- /dev/null +++ b/concat/spec/acceptance/quoted_paths_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper_acceptance' + +describe 'quoted paths' do + before(:all) do + shell('rm -rf "/tmp/concat test" /var/lib/puppet/concat') + shell('mkdir -p "/tmp/concat test"') + end + + context 'path with blanks' do + pp = <<-EOS + concat { '/tmp/concat test/foo': + } + concat::fragment { '1': + target => '/tmp/concat test/foo', + content => 'string1', + } + concat::fragment { '2': + target => '/tmp/concat test/foo', + content => 'string2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat test/foo') do + it { should be_file } + it { should contain "string1\nsring2" } + end + end +end diff --git a/concat/spec/acceptance/replace_spec.rb b/concat/spec/acceptance/replace_spec.rb new file mode 100644 index 000000000..7b31e09c4 --- /dev/null +++ b/concat/spec/acceptance/replace_spec.rb @@ -0,0 +1,241 @@ +require 'spec_helper_acceptance' + +describe 'replacement of' do + context 'file' do + context 'should not succeed' do + before(:all) do + shell('mkdir -p /tmp/concat') + shell('echo "file exists" > /tmp/concat/file') + end + after(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + end + + pp = <<-EOS + concat { '/tmp/concat/file': + replace => false, + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain 'file exists' } + it { should_not contain '1' } + it { should_not contain '2' } + end + end + + context 'should succeed' do + before(:all) do + shell('mkdir -p /tmp/concat') + shell('echo "file exists" > /tmp/concat/file') + end + after(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + end + + pp = <<-EOS + concat { '/tmp/concat/file': + replace => true, + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should_not contain 'file exists' } + it { should contain '1' } + it { should contain '2' } + end + end + end # file + + context 'symlink' do + context 'should not succeed' do + # XXX the core puppet file type will replace a symlink with a plain file + # when using ensure => present and source => ... but it will not when using + # ensure => present and content => ...; this is somewhat confusing behavior + before(:all) do + shell('mkdir -p /tmp/concat') + shell('ln -s /tmp/concat/dangling /tmp/concat/file') + end + after(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + end + + pp = <<-EOS + concat { '/tmp/concat/file': + replace => false, + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_linked_to '/tmp/concat/dangling' } + end + + describe file('/tmp/concat/dangling') do + # XXX serverspec does not have a matcher for 'exists' + it { should_not be_file } + it { should_not be_directory } + end + end + + context 'should succeed' do + # XXX the core puppet file type will replace a symlink with a plain file + # when using ensure => present and source => ... but it will not when using + # ensure => present and content => ...; this is somewhat confusing behavior + before(:all) do + shell('mkdir -p /tmp/concat') + shell('ln -s /tmp/concat/dangling /tmp/concat/file') + end + after(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + end + + pp = <<-EOS + concat { '/tmp/concat/file': + replace => true, + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain '1' } + it { should contain '2' } + end + end + end # symlink + + context 'directory' do + context 'should not succeed' do + before(:all) do + shell('mkdir -p /tmp/concat/file') + end + after(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + end + + pp = <<-EOS + concat { '/tmp/concat/file': } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + } + EOS + + it 'applies the manifest twice with stderr for changing to file' do + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/change from directory to file failed/) + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/change from directory to file failed/) + end + + describe file('/tmp/concat/file') do + it { should be_directory } + end + end + + # XXX concat's force param currently enables the creation of empty files + # when there are no fragments, and the replace param will only replace + # files and symlinks, not directories. The semantics either need to be + # changed, extended, or a new param introduced to control directory + # replacement. + context 'should succeed', :pending => 'not yet implemented' do + before(:all) do + shell('mkdir -p /tmp/concat/file') + end + after(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + end + + pp = <<-EOS + concat { '/tmp/concat/file': + force => true, + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain '1' } + end + end + end # directory +end diff --git a/concat/spec/acceptance/symbolic_name_spec.rb b/concat/spec/acceptance/symbolic_name_spec.rb new file mode 100644 index 000000000..7267f5e6b --- /dev/null +++ b/concat/spec/acceptance/symbolic_name_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper_acceptance' + +describe 'symbolic name' do + pp = <<-EOS + concat { 'not_abs_path': + path => '/tmp/concat/file', + } + + concat::fragment { '1': + target => 'not_abs_path', + content => '1', + order => '01', + } + + concat::fragment { '2': + target => 'not_abs_path', + content => '2', + order => '02', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain '1' } + it { should contain '2' } + end +end diff --git a/concat/spec/acceptance/warn_spec.rb b/concat/spec/acceptance/warn_spec.rb new file mode 100644 index 000000000..cb0b7430d --- /dev/null +++ b/concat/spec/acceptance/warn_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper_acceptance' + +describe 'concat warn =>' do + context 'true should enable default warning message' do + pp = <<-EOS + concat { '/tmp/concat/file': + warn => true, + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + order => '01', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + order => '02', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain '# This file is managed by Puppet. DO NOT EDIT.' } + it { should contain '1' } + it { should contain '2' } + end + end + context 'false should not enable default warning message' do + pp = <<-EOS + concat { '/tmp/concat/file': + warn => false, + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + order => '01', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + order => '02', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should_not contain '# This file is managed by Puppet. DO NOT EDIT.' } + it { should contain '1' } + it { should contain '2' } + end + end + context '# foo should overide default warning message' do + pp = <<-EOS + concat { '/tmp/concat/file': + warn => '# foo', + } + + concat::fragment { '1': + target => '/tmp/concat/file', + content => '1', + order => '01', + } + + concat::fragment { '2': + target => '/tmp/concat/file', + content => '2', + order => '02', + } + EOS + + it 'applies the manifest twice with no stderr' do + expect(apply_manifest(pp, :catch_failures => true).stderr).to eq("") + expect(apply_manifest(pp, :catch_changes => true).stderr).to eq("") + end + + describe file('/tmp/concat/file') do + it { should be_file } + it { should contain '# foo' } + it { should contain '1' } + it { should contain '2' } + end + end +end diff --git a/concat/spec/spec.opts b/concat/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/concat/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/concat/spec/spec_helper.rb b/concat/spec/spec_helper.rb new file mode 100644 index 000000000..2c6f56649 --- /dev/null +++ b/concat/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/concat/spec/spec_helper_acceptance.rb b/concat/spec/spec_helper_acceptance.rb new file mode 100644 index 000000000..22bd72f06 --- /dev/null +++ b/concat/spec/spec_helper_acceptance.rb @@ -0,0 +1,39 @@ +require 'beaker-rspec/spec_helper' +require 'beaker-rspec/helpers/serverspec' + +unless ENV['RS_PROVISION'] == 'no' or ENV['BEAKER_provision'] == 'no' + if hosts.first.is_pe? + install_pe + else + install_puppet + end + hosts.each do |host| + on hosts, "mkdir -p #{host['distmoduledir']}" + end +end + +RSpec.configure do |c| + # Project root + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Readable test descriptions + c.formatter = :documentation + + # Configure all nodes in nodeset + c.before :suite do + # Install module and dependencies + puppet_module_install(:source => proj_root, :module_name => 'concat') + hosts.each do |host| + on host, puppet('module','install','puppetlabs-stdlib'), { :acceptable_exit_codes => [0,1] } + end + end + + c.before(:all) do + shell('mkdir -p /tmp/concat') + end + c.after(:all) do + shell('rm -rf /tmp/concat /var/lib/puppet/concat') + end + + c.treat_symbols_as_metadata_keys_with_true_values = true +end diff --git a/concat/spec/unit/classes/concat_setup_spec.rb b/concat/spec/unit/classes/concat_setup_spec.rb new file mode 100644 index 000000000..3096e7368 --- /dev/null +++ b/concat/spec/unit/classes/concat_setup_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe 'concat::setup', :type => :class do + + shared_examples 'setup' do |concatdir| + concatdir = '/foo' if concatdir.nil? + + let(:facts) {{ :concat_basedir => concatdir }} + + it do + should contain_file("#{concatdir}/bin/concatfragments.sh").with({ + :mode => '0755', + :source => 'puppet:///modules/concat/concatfragments.sh', + :backup => false, + }) + end + + [concatdir, "#{concatdir}/bin"].each do |file| + it do + should contain_file(file).with({ + :ensure => 'directory', + :mode => '0755', + :backup => false, + }) + end + end + end + + context 'facts' do + context 'concat_basedir =>' do + context '/foo' do + it_behaves_like 'setup', '/foo' + end + end + end # facts + + context 'deprecated as a public class' do + it 'should create a warning' do + pending('rspec-puppet support for testing warning()') + end + end + + context "on osfamily Solaris" do + concatdir = '/foo' + let(:facts) do + { + :concat_basedir => concatdir, + :osfamily => 'Solaris', + :id => 'root', + } + end + + it do + should contain_file("#{concatdir}/bin/concatfragments.rb").with({ + :ensure => 'file', + :owner => 'root', + :mode => '0755', + :source => 'puppet:///modules/concat/concatfragments.rb', + :backup => false, + }) + end + end # on osfamily Solaris + + context "on osfamily windows" do + concatdir = '/foo' + let(:facts) do + { + :concat_basedir => concatdir, + :osfamily => 'windows', + :id => 'batman', + } + end + + it do + should contain_file("#{concatdir}/bin/concatfragments.rb").with({ + :ensure => 'file', + :owner => nil, + :mode => nil, + :source => 'puppet:///modules/concat/concatfragments.rb', + :backup => false, + }) + end + end # on osfamily windows +end diff --git a/concat/spec/unit/defines/concat_fragment_spec.rb b/concat/spec/unit/defines/concat_fragment_spec.rb new file mode 100644 index 000000000..3c61aabf7 --- /dev/null +++ b/concat/spec/unit/defines/concat_fragment_spec.rb @@ -0,0 +1,267 @@ +require 'spec_helper' + +describe 'concat::fragment', :type => :define do + + shared_examples 'fragment' do |title, params| + params = {} if params.nil? + + p = { + :content => nil, + :source => nil, + :order => 10, + :ensure => 'present', + }.merge(params) + + safe_name = title.gsub(/[\/\n]/, '_') + safe_target_name = p[:target].gsub(/[\/\n]/, '_') + concatdir = '/var/lib/puppet/concat' + fragdir = "#{concatdir}/#{safe_target_name}" + id = 'root' + if p[:ensure] == 'absent' + safe_ensure = p[:ensure] + else + safe_ensure = 'file' + end + + let(:title) { title } + let(:facts) {{ :concat_basedir => concatdir, :id => id }} + let(:params) { params } + let(:pre_condition) do + "concat{ '#{p[:target]}': }" + end + + it do + should contain_class('concat::setup') + should contain_concat(p[:target]) + should contain_file("#{fragdir}/fragments/#{p[:order]}_#{safe_name}").with({ + :ensure => safe_ensure, + :owner => id, + :mode => '0640', + :source => p[:source], + :content => p[:content], + :alias => "concat_fragment_#{title}", + :backup => false, + }) + end + end + + context 'title' do + ['0', '1', 'a', 'z'].each do |title| + it_behaves_like 'fragment', title, { + :target => '/etc/motd', + } + end + end # title + + context 'target =>' do + ['./etc/motd', 'etc/motd', 'motd_header'].each do |target| + context target do + it_behaves_like 'fragment', target, { + :target => '/etc/motd', + } + end + end + + context 'false' do + let(:title) { 'motd_header' } + let(:facts) {{ :concat_basedir => '/tmp' }} + let(:params) {{ :target => false }} + + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string/) + end + end + end # target => + + context 'ensure =>' do + ['present', 'absent'].each do |ens| + context ens do + it_behaves_like 'fragment', 'motd_header', { + :ensure => ens, + :target => '/etc/motd', + } + end + end + + context 'any value other than \'present\' or \'absent\'' do + let(:title) { 'motd_header' } + let(:facts) {{ :concat_basedir => '/tmp' }} + let(:params) {{ :ensure => 'invalid', :target => '/etc/motd' }} + + it 'should create a warning' do + pending('rspec-puppet support for testing warning()') + end + end + end # ensure => + + context 'content =>' do + ['', 'ashp is our hero'].each do |content| + context content do + it_behaves_like 'fragment', 'motd_header', { + :content => content, + :target => '/etc/motd', + } + end + end + + context 'false' do + let(:title) { 'motd_header' } + let(:facts) {{ :concat_basedir => '/tmp' }} + let(:params) {{ :content => false, :target => '/etc/motd' }} + + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string/) + end + end + end # content => + + context 'source =>' do + ['', '/foo/bar', ['/foo/bar', '/foo/baz']].each do |source| + context source do + it_behaves_like 'fragment', 'motd_header', { + :source => source, + :target => '/etc/motd', + } + end + end + + context 'false' do + let(:title) { 'motd_header' } + let(:facts) {{ :concat_basedir => '/tmp' }} + let(:params) {{ :source => false, :target => '/etc/motd' }} + + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string or an Array/) + end + end + end # source => + + context 'order =>' do + ['', '42', 'a', 'z'].each do |order| + context '\'\'' do + it_behaves_like 'fragment', 'motd_header', { + :order => order, + :target => '/etc/motd', + } + end + end + + context 'false' do + let(:title) { 'motd_header' } + let(:facts) {{ :concat_basedir => '/tmp' }} + let(:params) {{ :order => false, :target => '/etc/motd' }} + + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string or integer/) + end + end + end # order => + + context 'more than one content source' do + error_msg = 'You cannot specify more than one of $content, $source, $ensure => /target' + + context 'ensure => target and source' do + let(:title) { 'motd_header' } + let(:facts) {{ :concat_basedir => '/tmp' }} + let(:params) do + { + :target => '/etc/motd', + :ensure => '/foo', + :source => '/bar', + } + end + + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /#{Regexp.escape(error_msg)}/m) + end + end + + context 'ensure => target and content' do + let(:title) { 'motd_header' } + let(:facts) {{ :concat_basedir => '/tmp' }} + let(:params) do + { + :target => '/etc/motd', + :ensure => '/foo', + :content => 'bar', + } + end + + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /#{Regexp.escape(error_msg)}/m) + end + end + + context 'source and content' do + let(:title) { 'motd_header' } + let(:facts) {{ :concat_basedir => '/tmp' }} + let(:params) do + { + :target => '/etc/motd', + :source => '/foo', + :content => 'bar', + } + end + + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /#{Regexp.escape(error_msg)}/m) + end + end + + end # more than one content source + + describe 'deprecated parameter' do + context 'mode =>' do + context '1755' do + it_behaves_like 'fragment', 'motd_header', { + :mode => '1755', + :target => '/etc/motd', + } + + it 'should create a warning' do + pending('rspec-puppet support for testing warning()') + end + end + end # mode => + + context 'owner =>' do + context 'apenny' do + it_behaves_like 'fragment', 'motd_header', { + :owner => 'apenny', + :target => '/etc/motd', + } + + it 'should create a warning' do + pending('rspec-puppet support for testing warning()') + end + end + end # owner => + + context 'group =>' do + context 'apenny' do + it_behaves_like 'fragment', 'motd_header', { + :group => 'apenny', + :target => '/etc/motd', + } + + it 'should create a warning' do + pending('rspec-puppet support for testing warning()') + end + end + end # group => + + context 'backup =>' do + context 'foo' do + it_behaves_like 'fragment', 'motd_header', { + :backup => 'foo', + :target => '/etc/motd', + } + + it 'should create a warning' do + pending('rspec-puppet support for testing warning()') + end + end + end # backup => + end # deprecated params + +end diff --git a/concat/spec/unit/defines/concat_spec.rb b/concat/spec/unit/defines/concat_spec.rb new file mode 100644 index 000000000..9fdd7b26f --- /dev/null +++ b/concat/spec/unit/defines/concat_spec.rb @@ -0,0 +1,380 @@ +require 'spec_helper' + +describe 'concat', :type => :define do + + shared_examples 'concat' do |title, params, id| + params = {} if params.nil? + id = 'root' if id.nil? + + # default param values + p = { + :ensure => 'present', + :path => title, + :owner => nil, + :group => nil, + :mode => '0644', + :warn => false, + :force => false, + :backup => 'puppet', + :replace => true, + :order => 'alpha', + :ensure_newline => false, + }.merge(params) + + safe_name = title.gsub('/', '_') + concatdir = '/var/lib/puppet/concat' + fragdir = "#{concatdir}/#{safe_name}" + concat_name = 'fragments.concat.out' + default_warn_message = '# This file is managed by Puppet. DO NOT EDIT.' + + file_defaults = { + :backup => false, + } + + let(:title) { title } + let(:params) { params } + let(:facts) {{ :concat_basedir => concatdir, :id => id }} + + if p[:ensure] == 'present' + it do + should contain_file(fragdir).with(file_defaults.merge({ + :ensure => 'directory', + :mode => '0750', + })) + end + + it do + should contain_file("#{fragdir}/fragments").with(file_defaults.merge({ + :ensure => 'directory', + :mode => '0750', + :force => true, + :ignore => ['.svn', '.git', '.gitignore'], + :purge => true, + :recurse => true, + })) + end + + [ + "#{fragdir}/fragments.concat", + "#{fragdir}/#{concat_name}", + ].each do |file| + it do + should contain_file(file).with(file_defaults.merge({ + :ensure => 'present', + :mode => '0640', + })) + end + end + + it do + should contain_file(title).with(file_defaults.merge({ + :ensure => 'present', + :owner => p[:owner], + :group => p[:group], + :mode => p[:mode], + :replace => p[:replace], + :path => p[:path], + :alias => "concat_#{title}", + :source => "#{fragdir}/#{concat_name}", + :backup => p[:backup], + })) + end + + cmd = "#{concatdir}/bin/concatfragments.sh " + + "-o \"#{concatdir}/#{safe_name}/fragments.concat.out\" " + + "-d \"#{concatdir}/#{safe_name}\"" + + # flag order: fragdir, warnflag, forceflag, orderflag, newlineflag + if p.has_key?(:warn) + case p[:warn] + when TrueClass + message = default_warn_message + when 'true', 'yes', 'on' + # should generate a stringified boolean warning + message = default_warn_message + when FalseClass + message = nil + when 'false', 'no', 'off' + # should generate a stringified boolean warning + message = nil + else + message = p[:warn] + end + + unless message.nil? + cmd += " -w \'#{message}\'" + end + end + + cmd += " -f" if p[:force] + cmd += " -n" if p[:order] == 'numeric' + cmd += " -l" if p[:ensure_newline] == true + + it do + should contain_exec("concat_#{title}").with({ + :alias => "concat_#{fragdir}", + :command => cmd, + :unless => "#{cmd} -t", + }) + end + else + [ + fragdir, + "#{fragdir}/fragments", + "#{fragdir}/fragments.concat", + "#{fragdir}/#{concat_name}", + ].each do |file| + it do + should contain_file(file).with(file_defaults.merge({ + :ensure => 'absent', + :backup => false, + :force => true, + })) + end + end + + it do + should contain_file(title).with(file_defaults.merge({ + :ensure => 'absent', + :backup => p[:backup], + })) + end + + it do + should contain_exec("concat_#{title}").with({ + :alias => "concat_#{fragdir}", + :command => 'true', + :path => '/bin:/usr/bin', + }) + end + end + end + + context 'title' do + context 'without path param' do + # title/name is the default value for the path param. therefore, the + # title must be an absolute path unless path is specified + ['/foo', '/foo/bar', '/foo/bar/baz'].each do |title| + context title do + it_behaves_like 'concat', '/etc/foo.bar' + end + end + + ['./foo', 'foo', 'foo/bar'].each do |title| + context title do + let(:title) { title } + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not an absolute path/) + end + end + end + end + + context 'with path param' do + ['./foo', 'foo', 'foo/bar'].each do |title| + context title do + it_behaves_like 'concat', title, { :path => '/etc/foo.bar' } + end + end + end + end # title => + + context 'as non-root user' do + it_behaves_like 'concat', '/etc/foo.bar', {}, 'bob' + end + + context 'ensure =>' do + ['present', 'absent'].each do |ens| + context ens do + it_behaves_like 'concat', '/etc/foo.bar', { :ensure => ens } + end + end + + context 'invalid' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :ensure => 'invalid' }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /#{Regexp.escape('does not match "^present$|^absent$"')}/) + end + end + end # ensure => + + context 'path =>' do + context '/foo' do + it_behaves_like 'concat', '/etc/foo.bar', { :path => '/foo' } + end + + ['./foo', 'foo', 'foo/bar', false].each do |path| + context path do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :path => path }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not an absolute path/) + end + end + end + end # path => + + context 'owner =>' do + context 'apenney' do + it_behaves_like 'concat', '/etc/foo.bar', { :owner => 'apenny' } + end + + context 'false' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :owner => false }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string/) + end + end + end # owner => + + context 'group =>' do + context 'apenney' do + it_behaves_like 'concat', '/etc/foo.bar', { :group => 'apenny' } + end + + context 'false' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :group => false }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string/) + end + end + end # group => + + context 'mode =>' do + context '1755' do + it_behaves_like 'concat', '/etc/foo.bar', { :mode => '1755' } + end + + context 'false' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :mode => false }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string/) + end + end + end # mode => + + context 'warn =>' do + [true, false, '# foo'].each do |warn| + context warn do + it_behaves_like 'concat', '/etc/foo.bar', { :warn => warn } + end + end + + context '(stringified boolean)' do + ['true', 'yes', 'on', 'false', 'no', 'off'].each do |warn| + context warn do + it_behaves_like 'concat', '/etc/foo.bar', { :warn => warn } + + it 'should create a warning' do + pending('rspec-puppet support for testing warning()') + end + end + end + end + + context '123' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :warn => 123 }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string or boolean/) + end + end + end # warn => + + context 'force =>' do + [true, false].each do |force| + context force do + it_behaves_like 'concat', '/etc/foo.bar', { :force => force } + end + end + + context '123' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :force => 123 }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a boolean/) + end + end + end # force => + + context 'backup =>' do + context 'reverse' do + it_behaves_like 'concat', '/etc/foo.bar', { :backup => 'reverse' } + end + + context 'false' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :backup => false }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a string/) + end + end + end # backup => + + context 'replace =>' do + [true, false].each do |replace| + context replace do + it_behaves_like 'concat', '/etc/foo.bar', { :replace => replace } + end + end + + context '123' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :replace => 123 }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a boolean/) + end + end + end # replace => + + context 'order =>' do + ['alpha', 'numeric'].each do |order| + context order do + it_behaves_like 'concat', '/etc/foo.bar', { :order => order } + end + end + + context 'invalid' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :order => 'invalid' }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /#{Regexp.escape('does not match "^alpha$|^numeric$"')}/) + end + end + end # order => + + context 'ensure_newline =>' do + [true, false].each do |ensure_newline| + context 'true' do + it_behaves_like 'concat', '/etc/foo.bar', { :ensure_newline => ensure_newline} + end + end + + context '123' do + let(:title) { '/etc/foo.bar' } + let(:params) {{ :ensure_newline => 123 }} + it 'should fail' do + expect { should }.to raise_error(Puppet::Error, /is not a boolean/) + end + end + end # ensure_newline => + + describe 'deprecated parameter' do + context 'gnu =>' do + context 'foo' do + it_behaves_like 'concat', '/etc/foo.bar', { :gnu => 'foo'} + + it 'should create a warning' do + pending('rspec-puppet support for testing warning()') + end + end + end + end + +end + +# vim:sw=2:ts=2:expandtab:textwidth=79 diff --git a/concat/spec/unit/facts/concat_basedir_spec.rb b/concat/spec/unit/facts/concat_basedir_spec.rb new file mode 100644 index 000000000..41bc90f15 --- /dev/null +++ b/concat/spec/unit/facts/concat_basedir_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe 'concat_basedir', :type => :fact do + before(:each) { Facter.clear } + + context 'Puppet[:vardir] ==' do + it '/var/lib/puppet' do + Puppet.stubs(:[]).with(:vardir).returns('/var/lib/puppet') + Facter.fact(:concat_basedir).value.should == '/var/lib/puppet/concat' + end + + it '/home/apenny/.puppet/var' do + Puppet.stubs(:[]).with(:vardir).returns('/home/apenny/.puppet/var') + Facter.fact(:concat_basedir).value.should == '/home/apenny/.puppet/var/concat' + end + end + +end diff --git a/concat/tests/fragment.pp b/concat/tests/fragment.pp new file mode 100644 index 000000000..a2dfaca29 --- /dev/null +++ b/concat/tests/fragment.pp @@ -0,0 +1,19 @@ +concat { 'testconcat': + ensure => present, + path => '/tmp/concat', + owner => 'root', + group => 'root', + mode => '0664', +} + +concat::fragment { '1': + target => 'testconcat', + content => '1', + order => '01', +} + +concat::fragment { '2': + target => 'testconcat', + content => '2', + order => '02', +} diff --git a/concat/tests/init.pp b/concat/tests/init.pp new file mode 100644 index 000000000..fd2142718 --- /dev/null +++ b/concat/tests/init.pp @@ -0,0 +1,7 @@ +concat { '/tmp/concat': + ensure => present, + force => true, + owner => 'root', + group => 'root', + mode => '0644', +} diff --git a/firewall b/firewall deleted file mode 160000 index d5a10f5a5..000000000 --- a/firewall +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d5a10f5a52d84b9fcfb8fc65ef505685a07d5799 diff --git a/firewall/.fixtures.yml b/firewall/.fixtures.yml new file mode 100644 index 000000000..0d10d5cec --- /dev/null +++ b/firewall/.fixtures.yml @@ -0,0 +1,3 @@ +fixtures: + symlinks: + "firewall": "#{source_dir}" diff --git a/firewall/.gitignore b/firewall/.gitignore new file mode 100644 index 000000000..b5b7a00d6 --- /dev/null +++ b/firewall/.gitignore @@ -0,0 +1,7 @@ +pkg/ +Gemfile.lock +vendor/ +spec/fixtures/ +.vagrant/ +.bundle/ +coverage/ diff --git a/firewall/.nodeset.yml b/firewall/.nodeset.yml new file mode 100644 index 000000000..767f9cd2f --- /dev/null +++ b/firewall/.nodeset.yml @@ -0,0 +1,31 @@ +--- +default_set: 'centos-64-x64' +sets: + 'centos-59-x64': + nodes: + "main.foo.vm": + prefab: 'centos-59-x64' + 'centos-64-x64': + nodes: + "main.foo.vm": + prefab: 'centos-64-x64' + 'fedora-18-x64': + nodes: + "main.foo.vm": + prefab: 'fedora-18-x64' + 'debian-607-x64': + nodes: + "main.foo.vm": + prefab: 'debian-607-x64' + 'debian-70rc1-x64': + nodes: + "main.foo.vm": + prefab: 'debian-70rc1-x64' + 'ubuntu-server-10044-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-10044-x64' + 'ubuntu-server-12042-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-12042-x64' diff --git a/firewall/.sync.yml b/firewall/.sync.yml new file mode 100644 index 000000000..66a03c649 --- /dev/null +++ b/firewall/.sync.yml @@ -0,0 +1,3 @@ +--- +spec/spec_helper.rb: + unmanaged: true diff --git a/firewall/.travis.yml b/firewall/.travis.yml new file mode 100644 index 000000000..a40ae502e --- /dev/null +++ b/firewall/.travis.yml @@ -0,0 +1,17 @@ +--- +language: ruby +bundler_args: --without development +script: "bundle exec rake validate && bundle exec rake lint && bundle exec rake spec SPEC_OPTS='--format documentation'" +matrix: + fast_finish: true + include: + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.6.0" + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.7.0" + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 3.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 3.0" +notifications: + email: false diff --git a/firewall/CHANGELOG.md b/firewall/CHANGELOG.md new file mode 100644 index 000000000..c8c3f3ebf --- /dev/null +++ b/firewall/CHANGELOG.md @@ -0,0 +1,444 @@ +##2014-07-08 - Supported Release 1.1.3 +###Summary +This is a supported release with test coverage enhancements. + +####Bugfixes +- Confine to supported kernels + +##2014-06-04 - Release 1.1.2 +###Summary + +This is a release of the code previously released as 1.1.1, with updated metadata. + +## 2014-05-16 Release 1.1.1 +###Summary + +This release reverts the alphabetical ordering of 1.1.0. We found this caused +a regression in the Openstack modules so in the interest of safety we have +removed this for now. + +## 2014-05-13 Release 1.1.0 +###Summary + +This release has a significant change from previous releases; we now apply the +firewall resources alphabetically by default, removing the need to create pre +and post classes just to enforce ordering. It only effects default ordering +and further information can be found in the README about this. Please test +this in development before rolling into production out of an abundance of +caution. + +We've also added `mask` which is required for --recent in recent (no pun +intended) versions of iptables, as well as connlimit and connmark. This +release has been validated against Ubuntu 14.04 and RHEL7 and should be fully +working on those platforms. + +####Features + +- Apply firewall resources alphabetically. +- Add support for connlimit and connmark. +- Add `mask` as a parameter. (Used exclusively with the recent parameter). + +####Bugfixes + +- Add systemd support for RHEL7. +- Replace &&'s with the correct and in manifests. +- Fix tests on Trusty and RHEL7 +- Fix for Fedora Rawhide. +- Fix boolean flag tests. +- Fix DNAT->SNAT typo in an error message. + +####Known Bugs + +* For Oracle, the `owner` and `socket` parameters require a workaround to function. Please see the Limitations section of the README. + + +## 2014-03-04 Supported Release 1.0.2 +###Summary + +This is a supported release. This release removes a testing symlink that can +cause trouble on systems where /var is on a seperate filesystem from the +modulepath. + +####Features +####Bugfixes +####Known Bugs + +* For Oracle, the `owner` and `socket` parameters require a workaround to function. Please see the Limitations section of the README. + +### Supported release - 2014-03-04 1.0.1 + +####Summary + +An important bugfix was made to the offset calculation for unmanaged rules +to handle rules with 9000+ in the name. + +####Features + +####Bugfixes +- Offset calculations assumed unmanaged rules were numbered 9000+. +- Gracefully fail to manage ip6tables on iptables 1.3.x + +####Known Bugs + +* For Oracle, the `owner` and `socket` parameters require a workaround to function. Please see the Limitations section of the README. + +--- +### 1.0.0 - 2014-02-11 + +No changes, just renumbering to 1.0.0. + +--- +### 0.5.0 - 2014-02-10 + +##### Summary: +This is a bigger release that brings in "recent" connection limiting (think +"port knocking"), firewall chain purging on a per-chain/per-table basis, and +support for a few other use cases. This release also fixes a major bug which +could cause modifications to the wrong rules when unmanaged rules are present. + +##### New Features: +* Add "recent" limiting via parameters `rdest`, `reap`, `recent`, `rhitcount`, + `rname`, `rseconds`, `rsource`, and `rttl` +* Add negation support for source and destination +* Add per-chain/table purging support to `firewallchain` +* IPv4 specific + * Add random port forwarding support + * Add ipsec policy matching via `ipsec_dir` and `ipsec_policy` +* IPv6 specific + * Add support for hop limiting via `hop_limit` parameter + * Add fragmentation matchers via `ishasmorefrags`, `islastfrag`, and `isfirstfrag` + * Add support for conntrack stateful firewall matching via `ctstate` + +##### Bugfixes: +- Boolean fixups allowing false values +- Better detection of unmanaged rules +- Fix multiport rule detection +- Fix sport/dport rule detection +- Make INPUT, OUTPUT, and FORWARD not autorequired for firewall chain filter +- Allow INPUT with the nat table +- Fix `src_range` & `dst_range` order detection +- Documentation clarifications +- Fixes to spec tests + +--------------------------------------- + +### 0.4.2 - 2013-09-10 + +Another attempt to fix the packaging issue. We think we understand exactly +what is failing and this should work properly for the first time. + +--------------------------------------- + +### 0.4.1 - 2013-08-09 + +Bugfix release to fix a packaging issue that may have caused puppet module +install commands to fail. + +--------------------------------------- + +### 0.4.0 - 2013-07-11 + +This release adds support for address type, src/dest ip ranges, and adds +additional testing and bugfixes. + +#### Features +* Add `src_type` and `dst_type` attributes (Nick Stenning) +* Add `src_range` and `dst_range` attributes (Lei Zhang) +* Add SL and SLC operatingsystems as supported (Steve Traylen) + +#### Bugfixes +* Fix parser for bursts other than 5 (Chris Rutter) +* Fix parser for -f in --comment (Georg Koester) +* Add doc headers to class files (Dan Carley) +* Fix lint warnings/errors (Wolf Noble) + +--------------------------------------- + +### 0.3.1 - 2013/6/10 + +This minor release provides some bugfixes and additional tests. + +#### Changes + +* Update tests for rspec-system-puppet 2 (Ken Barber) +* Update rspec-system tests for rspec-system-puppet 1.5 (Ken Barber) +* Ensure all services have 'hasstatus => true' for Puppet 2.6 (Ken Barber) +* Accept pre-existing rule with invalid name (Joe Julian) +* Swap log_prefix and log_level order to match the way it's saved (Ken Barber) +* Fix log test to replicate bug #182 (Ken Barber) +* Split argments while maintaining quoted strings (Joe Julian) +* Add more log param tests (Ken Barber) +* Add extra tests for logging parameters (Ken Barber) +* Clarify OS support (Ken Barber) + +--------------------------------------- + +### 0.3.0 - 2013/4/25 + +This release introduces support for Arch Linux and extends support for Fedora 15 and up. There are also lots of bugs fixed and improved testing to prevent regressions. + +##### Changes + +* Fix error reporting for insane hostnames (Tomas Doran) +* Support systemd on Fedora 15 and up (Eduardo Gutierrez) +* Move examples to docs (Ken Barber) +* Add support for Arch Linux platform (Ingmar Steen) +* Add match rule for fragments (Georg Koester) +* Fix boolean rules being recognized as changed (Georg Koester) +* Same rules now get deleted (Anastasis Andronidis) +* Socket params test (Ken Barber) +* Ensure parameter can disable firewall (Marc Tardif) + +--------------------------------------- + +### 0.2.1 - 2012/3/13 + +This maintenance release introduces the new README layout, and fixes a bug with iptables_persistent_version. + +##### Changes + +* (GH-139) Throw away STDERR from dpkg-query in Fact +* Update README to be consistent with module documentation template +* Fix failing spec tests due to dpkg change in iptables_persistent_version + +--------------------------------------- + +### 0.2.0 - 2012/3/3 + +This release introduces automatic persistence, removing the need for the previous manual dependency requirement for persistent the running rules to the OS persistence file. + +Previously you would have required the following in your site.pp (or some other global location): + + # Always persist firewall rules + exec { 'persist-firewall': + command => $operatingsystem ? { + 'debian' => '/sbin/iptables-save > /etc/iptables/rules.v4', + /(RedHat|CentOS)/ => '/sbin/iptables-save > /etc/sysconfig/iptables', + }, + refreshonly => true, + } + Firewall { + notify => Exec['persist-firewall'], + before => Class['my_fw::post'], + require => Class['my_fw::pre'], + } + Firewallchain { + notify => Exec['persist-firewall'], + } + resources { "firewall": + purge => true + } + +You only need: + + class { 'firewall': } + Firewall { + before => Class['my_fw::post'], + require => Class['my_fw::pre'], + } + +To install pre-requisites and to create dependencies on your pre & post rules. Consult the README for more information. + +##### Changes + +* Firewall class manifests (Dan Carley) +* Firewall and firewallchain persistence (Dan Carley) +* (GH-134) Autorequire iptables related packages (Dan Carley) +* Typo in #persist_iptables OS normalisation (Dan Carley) +* Tests for #persist_iptables (Dan Carley) +* (GH-129) Replace errant return in autoreq block (Dan Carley) + +--------------------------------------- + +### 0.1.1 - 2012/2/28 + +This release primarily fixes changing parameters in 3.x + +##### Changes + +* (GH-128) Change method_missing usage to define_method for 3.x compatibility +* Update travis.yml gem specifications to actually test 2.6 +* Change source in Gemfile to use a specific URL for Ruby 2.0.0 compatibility + +--------------------------------------- + +### 0.1.0 - 2012/2/24 + +This release is somewhat belated, so no summary as there are far too many changes this time around. Hopefully we won't fall this far behind again :-). + +##### Changes + +* Add support for MARK target and set-mark property (Johan Huysmans) +* Fix broken call to super for ruby-1.9.2 in munge (Ken Barber) +* simple fix of the error message for allowed values of the jump property (Daniel Black) +* Adding OSPF(v3) protocol to puppetlabs-firewall (Arnoud Vermeer) +* Display multi-value: port, sport, dport and state command seperated (Daniel Black) +* Require jump=>LOG for log params (Daniel Black) +* Reject and document icmp => "any" (Dan Carley) +* add firewallchain type and iptables_chain provider (Daniel Black) +* Various fixes for firewallchain resource (Ken Barber) +* Modify firewallchain name to be chain:table:protocol (Ken Barber) +* Fix allvalidchain iteration (Ken Barber) +* Firewall autorequire Firewallchains (Dan Carley) +* Tests and docstring for chain autorequire (Dan Carley) +* Fix README so setup instructions actually work (Ken Barber) +* Support vlan interfaces (interface containing ".") (Johan Huysmans) +* Add tests for VLAN support for iniface/outiface (Ken Barber) +* Add the table when deleting rules (Johan Huysmans) +* Fix tests since we are now prefixing -t) +* Changed 'jump' to 'action', commands to lower case (Jason Short) +* Support interface names containing "+" (Simon Deziel) +* Fix for when iptables-save spews out "FATAL" errors (Sharif Nassar) +* Fix for incorrect limit command arguments for ip6tables provider (Michael Hsu) +* Document Util::Firewall.host_to_ip (Dan Carley) +* Nullify addresses with zero prefixlen (Dan Carley) +* Add support for --tcp-flags (Thomas Vander Stichele) +* Make tcp_flags support a feature (Ken Barber) +* OUTPUT is a valid chain for the mangle table (Adam Gibbins) +* Enable travis-ci support (Ken Barber) +* Convert an existing test to CIDR (Dan Carley) +* Normalise iptables-save to CIDR (Dan Carley) +* be clearer about what distributions we support (Ken Barber) +* add gre protocol to list of acceptable protocols (Jason Hancock) +* Added pkttype property (Ashley Penney) +* Fix mark to not repeat rules with iptables 1.4.1+ (Sharif Nassar) +* Stub iptables_version for now so tests run on non-Linux hosts (Ken Barber) +* Stub iptables facts for set_mark tests (Dan Carley) +* Update formatting of README to meet Puppet Labs best practices (Will Hopper) +* Support for ICMP6 type code resolutions (Dan Carley) +* Insert order hash included chains from different tables (Ken Barber) +* rspec 2.11 compatibility (Jonathan Boyett) +* Add missing class declaration in README (sfozz) +* array_matching is contraindicated (Sharif Nassar) +* Convert port Fixnum into strings (Sharif Nassar) +* Update test framework to the modern age (Ken Barber) +* working with ip6tables support (wuwx) +* Remove gemfile.lock and add to gitignore (William Van Hevelingen) +* Update travis and gemfile to be like stdlib travis files (William Van Hevelingen) +* Add support for -m socket option (Ken Barber) +* Add support for single --sport and --dport parsing (Ken Barber) +* Fix tests for Ruby 1.9.3 from 3e13bf3 (Dan Carley) +* Mock Resolv.getaddress in #host_to_ip (Dan Carley) +* Update docs for source and dest - they are not arrays (Ken Barber) + +--------------------------------------- + +### 0.0.4 - 2011/12/05 + +This release adds two new parameters, 'uid' and 'gid'. As a part of the owner module, these params allow you to specify a uid, username, gid, or group got a match: + + firewall { '497 match uid': + port => '123', + proto => 'mangle', + chain => 'OUTPUT', + action => 'drop' + uid => '123' + } + +This release also adds value munging for the 'log_level', 'source', and 'destination' parameters. The 'source' and 'destination' now support hostnames: + + firewall { '498 accept from puppetlabs.com': + port => '123', + proto => 'tcp', + source => 'puppetlabs.com', + action => 'accept' + } + + +The 'log_level' parameter now supports using log level names, such as 'warn', 'debug', and 'panic': + + firewall { '499 logging': + port => '123', + proto => 'udp', + log_level => 'debug', + action => 'drop' + } + +Additional changes include iptables and ip6tables version facts, general whitespace cleanup, and adding additional unit tests. + +##### Changes + +* (#10957) add iptables_version and ip6tables_version facts +* (#11093) Improve log_level property so it converts names to numbers +* (#10723) Munge hostnames and IPs to IPs with CIDR +* (#10718) Add owner-match support +* (#10997) Add fixtures for ipencap +* (#11034) Whitespace cleanup +* (#10690) add port property support to ip6tables + +--------------------------------------- + +### 0.0.3 - 2011/11/12 + +This release introduces a new parameter 'port' which allows you to set both +source and destination ports for a match: + + firewall { "500 allow NTP requests": + port => "123", + proto => "udp", + action => "accept", + } + +We also have the limit parameter finally working: + + firewall { "500 limit HTTP requests": + dport => 80, + proto => tcp, + limit => "60/sec", + burst => 30, + action => accept, + } + +State ordering has been fixed now, and more characters are allowed in the +namevar: + +* Alphabetical +* Numbers +* Punctuation +* Whitespace + +##### Changes + +* (#10693) Ensure -m limit is added for iptables when using 'limit' param +* (#10690) Create new port property +* (#10700) allow additional characters in comment string +* (#9082) Sort iptables --state option values internally to keep it consistent across runs +* (#10324) Remove extraneous whitespace from iptables rule line in spec tests + +--------------------------------------- + +### 0.0.2 - 2011/10/26 + +This is largely a maintanence and cleanup release, but includes the ability to +specify ranges of ports in the sport/dport parameter: + + firewall { "500 allow port range": + dport => ["3000-3030","5000-5050"], + sport => ["1024-65535"], + action => "accept", + } + +##### Changes + +* (#10295) Work around bug #4248 whereby the puppet/util paths are not being loaded correctly on the puppetmaster +* (#10002) Change to dport and sport to handle ranges, and fix handling of name to name to port +* (#10263) Fix tests on Puppet 2.6.x +* (#10163) Cleanup some of the inline documentation and README file to align with general forge usage + +--------------------------------------- + +### 0.0.1 - 2011/10/18 + +Initial release. + +##### Changes + +* (#9362) Create action property and perform transformation for accept, drop, reject value for iptables jump parameter +* (#10088) Provide a customised version of CONTRIBUTING.md +* (#10026) Re-arrange provider and type spec files to align with Puppet +* (#10026) Add aliases for test,specs,tests to Rakefile and provide -T as default +* (#9439) fix parsing and deleting existing rules +* (#9583) Fix provider detection for gentoo and unsupported linuxes for the iptables provider +* (#9576) Stub provider so it works properly outside of Linux +* (#9576) Align spec framework with Puppet core +* and lots of other earlier development tasks ... diff --git a/firewall/CONTRIBUTING.md b/firewall/CONTRIBUTING.md new file mode 100644 index 000000000..e1288478a --- /dev/null +++ b/firewall/CONTRIBUTING.md @@ -0,0 +1,234 @@ +Checklist (and a short version for the impatient) +================================================= + + * Commits: + + - Make commits of logical units. + + - Check for unnecessary whitespace with "git diff --check" before + committing. + + - Commit using Unix line endings (check the settings around "crlf" in + git-config(1)). + + - Do not check in commented out code or unneeded files. + + - The first line of the commit message should be a short + description (50 characters is the soft limit, excluding ticket + number(s)), and should skip the full stop. + + - Associate the issue in the message. The first line should include + the issue number in the form "(#XXXX) Rest of message". + + - The body should provide a meaningful commit message, which: + + - uses the imperative, present tense: "change", not "changed" or + "changes". + + - includes motivation for the change, and contrasts its + implementation with the previous behavior. + + - Make sure that you have tests for the bug you are fixing, or + feature you are adding. + + - Make sure the test suites passes after your commit: + `bundle exec rspec spec/acceptance` More information on [testing](#Testing) below + + - When introducing a new feature, make sure it is properly + documented in the README.md + + * Submission: + + * Pre-requisites: + + - Sign the [Contributor License Agreement](https://cla.puppetlabs.com/) + + - Make sure you have a [GitHub account](https://github.com/join) + + - [Create a ticket](http://projects.puppetlabs.com/projects/modules/issues/new), or [watch the ticket](http://projects.puppetlabs.com/projects/modules/issues) you are patching for. + + * Preferred method: + + - Fork the repository on GitHub. + + - Push your changes to a topic branch in your fork of the + repository. (the format ticket/1234-short_description_of_change is + usually preferred for this project). + + - Submit a pull request to the repository in the puppetlabs + organization. + +The long version +================ + + 1. Make separate commits for logically separate changes. + + Please break your commits down into logically consistent units + which include new or changed tests relevant to the rest of the + change. The goal of doing this is to make the diff easier to + read for whoever is reviewing your code. In general, the easier + your diff is to read, the more likely someone will be happy to + review it and get it into the code base. + + If you are going to refactor a piece of code, please do so as a + separate commit from your feature or bug fix changes. + + We also really appreciate changes that include tests to make + sure the bug is not re-introduced, and that the feature is not + accidentally broken. + + Describe the technical detail of the change(s). If your + description starts to get too long, that is a good sign that you + probably need to split up your commit into more finely grained + pieces. + + Commits which plainly describe the things which help + reviewers check the patch and future developers understand the + code are much more likely to be merged in with a minimum of + bike-shedding or requested changes. Ideally, the commit message + would include information, and be in a form suitable for + inclusion in the release notes for the version of Puppet that + includes them. + + Please also check that you are not introducing any trailing + whitespace or other "whitespace errors". You can do this by + running "git diff --check" on your changes before you commit. + + 2. Sign the Contributor License Agreement + + Before we can accept your changes, we do need a signed Puppet + Labs Contributor License Agreement (CLA). + + You can access the CLA via the [Contributor License Agreement link](https://cla.puppetlabs.com/) + + If you have any questions about the CLA, please feel free to + contact Puppet Labs via email at cla-submissions@puppetlabs.com. + + 3. Sending your patches + + To submit your changes via a GitHub pull request, we _highly_ + recommend that you have them on a topic branch, instead of + directly on "master". + It makes things much easier to keep track of, especially if + you decide to work on another thing before your first change + is merged in. + + GitHub has some pretty good + [general documentation](http://help.github.com/) on using + their site. They also have documentation on + [creating pull requests](http://help.github.com/send-pull-requests/). + + In general, after pushing your topic branch up to your + repository on GitHub, you can switch to the branch in the + GitHub UI and click "Pull Request" towards the top of the page + in order to open a pull request. + + + 4. Update the related GitHub issue. + + If there is a GitHub issue associated with the change you + submitted, then you should update the ticket to include the + location of your branch, along with any other commentary you + may wish to make. + +Testing +======= + +Getting Started +--------------- + +Our puppet modules provide [`Gemfile`](./Gemfile)s which can tell a ruby +package manager such as [bundler](http://bundler.io/) what Ruby packages, +or Gems, are required to build, develop, and test this software. + +Please make sure you have [bundler installed](http://bundler.io/#getting-started) +on your system, then use it to install all dependencies needed for this project, +by running + +```shell +% bundle install +Fetching gem metadata from https://rubygems.org/........ +Fetching gem metadata from https://rubygems.org/.. +Using rake (10.1.0) +Using builder (3.2.2) +-- 8><-- many more --><8 -- +Using rspec-system-puppet (2.2.0) +Using serverspec (0.6.3) +Using rspec-system-serverspec (1.0.0) +Using bundler (1.3.5) +Your bundle is complete! +Use `bundle show [gemname]` to see where a bundled gem is installed. +``` + +NOTE some systems may require you to run this command with sudo. + +If you already have those gems installed, make sure they are up-to-date: + +```shell +% bundle update +``` + +With all dependencies in place and up-to-date we can now run the tests: + +```shell +% rake spec +``` + +This will execute all the [rspec tests](http://rspec-puppet.com/) tests +under [spec/defines](./spec/defines), [spec/classes](./spec/classes), +and so on. rspec tests may have the same kind of dependencies as the +module they are testing. While the module defines in its [Modulefile](./Modulefile), +rspec tests define them in [.fixtures.yml](./fixtures.yml). + +Some puppet modules also come with [beaker](https://github.com/puppetlabs/beaker) +tests. These tests spin up a virtual machine under +[VirtualBox](https://www.virtualbox.org/)) with, controlling it with +[Vagrant](http://www.vagrantup.com/) to actually simulate scripted test +scenarios. In order to run these, you will need both of those tools +installed on your system. + +You can run them by issuing the following command + +```shell +% rake spec_clean +% rspec spec/acceptance +``` + +This will now download a pre-fabricated image configured in the [default node-set](./spec/acceptance/nodesets/default.yml), +install puppet, copy this module and install its dependencies per [spec/spec_helper_acceptance.rb](./spec/spec_helper_acceptance.rb) +and then run all the tests under [spec/acceptance](./spec/acceptance). + +Writing Tests +------------- + +XXX getting started writing tests. + +If you have commit access to the repository +=========================================== + +Even if you have commit access to the repository, you will still need to +go through the process above, and have someone else review and merge +in your changes. The rule is that all changes must be reviewed by a +developer on the project (that did not write the code) to ensure that +all changes go through a code review process. + +Having someone other than the author of the topic branch recorded as +performing the merge is the record that they performed the code +review. + + +Additional Resources +==================== + +* [Getting additional help](http://projects.puppetlabs.com/projects/puppet/wiki/Getting_Help) + +* [Writing tests](http://projects.puppetlabs.com/projects/puppet/wiki/Development_Writing_Tests) + +* [Patchwork](https://patchwork.puppetlabs.com) + +* [Contributor License Agreement](https://projects.puppetlabs.com/contributor_licenses/sign) + +* [General GitHub documentation](http://help.github.com/) + +* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) + diff --git a/firewall/Gemfile b/firewall/Gemfile new file mode 100644 index 000000000..e960f7c4b --- /dev/null +++ b/firewall/Gemfile @@ -0,0 +1,27 @@ +source ENV['GEM_SOURCE'] || "https://rubygems.org" + +group :development, :test do + gem 'rake', :require => false + gem 'rspec-puppet', :require => false + gem 'puppetlabs_spec_helper', :require => false + gem 'serverspec', :require => false + gem 'puppet-lint', :require => false + gem 'beaker', :require => false + gem 'beaker-rspec', :require => false + gem 'pry', :require => false + gem 'simplecov', :require => false +end + +if facterversion = ENV['FACTER_GEM_VERSION'] + gem 'facter', facterversion, :require => false +else + gem 'facter', :require => false +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/firewall/LICENSE b/firewall/LICENSE new file mode 100644 index 000000000..1d196fc30 --- /dev/null +++ b/firewall/LICENSE @@ -0,0 +1,25 @@ +Puppet Firewall Module - Puppet module for managing Firewalls + +Copyright (C) 2011-2013 Puppet Labs, Inc. +Copyright (C) 2011 Jonathan Boyett +Copyright (C) 2011 Media Temple, Inc. + +Some of the iptables code was taken from puppet-iptables which was: + +Copyright (C) 2011 Bob.sh Limited +Copyright (C) 2008 Camptocamp Association +Copyright (C) 2007 Dmitri Priimak + +Puppet Labs can be contacted at: info@puppetlabs.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/firewall/README.markdown b/firewall/README.markdown new file mode 100644 index 000000000..d76d7b98e --- /dev/null +++ b/firewall/README.markdown @@ -0,0 +1,730 @@ +#firewall + +[![Build Status](https://travis-ci.org/puppetlabs/puppetlabs-firewall.png?branch=master)](https://travis-ci.org/puppetlabs/puppetlabs-firewall) + +####Table of Contents + +1. [Overview - What is the Firewall module?](#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with Firewall](#setup) + * [What Firewall Affects](#what-firewall-affects) + * [Setup Requirements](#setup-requirements) + * [Beginning with Firewall](#beginning-with-firewall) + * [Upgrading](#upgrading) +4. [Usage - Configuration and customization options](#usage) + * [Default rules - Setting up general configurations for all firewalls](#default-rules) + * [Application-Specific Rules - Options for configuring and managing firewalls across applications](#application-specific-rules) + * [Additional Uses for the Firewall Module](#other-rules) +5. [Reference - An under-the-hood peek at what the module is doing](#reference) +6. [Limitations - OS compatibility, etc.](#limitations) +7. [Development - Guide for contributing to the module](#development) + * [Tests - Testing your configuration](#tests) + +##Overview + +The Firewall module lets you manage firewall rules with Puppet. + +##Module Description + +PuppetLabs' Firewall module introduces the `firewall` resource, which is used to manage and configure firewall rules from within the Puppet DSL. This module offers support for iptables and ip6tables. The module also introduces the `firewallchain` resource, which allows you to manage chains or firewall lists and ebtables for bridging support. At the moment, only iptables and ip6tables chains are supported. + +The Firewall module acts on your running firewall, making immediate changes as the catalog executes. Defining default pre and post rules allows you to provide global defaults for your hosts before and after any custom rules. Defining `pre` and `post` rules is also necessary to help you avoid locking yourself out of your own boxes when Puppet runs. + +##Setup + +###What Firewall Affects + +* Every node running a firewall +* Firewall settings in your system +* Connection settings for managed nodes +* Unmanaged resources (get purged) + + +###Setup Requirements + +Firewall uses Ruby-based providers, so you must enable [pluginsync enabled](http://docs.puppetlabs.com/guides/plugins_in_modules.html#enabling-pluginsync). + +###Beginning with Firewall + +In the following two sections, you create new classes and then create firewall rules related to those classes. These steps are optional, but provide a framework for firewall rules, which is helpful if you’re just starting to create them. + +If you already have rules in place, then you don’t need to do these two sections. However, be aware of the ordering of your firewall rules. The module will dynamically apply rules in the order they appear in the catalog, meaning a deny rule could be applied before the allow rules. This might mean the module hasn’t established some of the important connections, such as the connection to the Puppet master. + +The following steps are designed to ensure that you keep your SSH and other connections, primarily your connection to your Puppet master. If you create the `pre` and `post` classes described in the first section, then you also need to create the rules described in the second section. + +####Create the `my_fw::pre` and `my_fw::post` Classes + +This approach employs a whitelist setup, so you can define what rules you want and everything else is ignored rather than removed. + +The code in this section does the following: + +* The `require` parameter in `Firewall {}` ensures `my_fw::pre` is run before any other rules. +* In the `my_fw::post` class declaration, the `before` parameter ensures `my_fw::post` is run after any other rules. + +Therefore, the run order is: + +* The rules in `my_fw::pre` +* Your rules (defined in code) +* The rules in `my_fw::post` + +The rules in the `pre` and `post` classes are fairly general. These two classes ensure that you retain connectivity, and that you drop unmatched packets appropriately. The rules you define in your manifests are likely specific to the applications you run. + +1. Add the `pre` class to `my_fw/manifests/pre.pp`. `pre.pp` should contain any default rules to be applied first. The rules in this class should be added in the order you want them to run. + + class my_fw::pre { + Firewall { + require => undef, + } + + # Default firewall rules + firewall { '000 accept all icmp': + proto => 'icmp', + action => 'accept', + }-> + firewall { '001 accept all to lo interface': + proto => 'all', + iniface => 'lo', + action => 'accept', + }-> + firewall { '002 accept related established rules': + proto => 'all', + ctstate => ['RELATED', 'ESTABLISHED'], + action => 'accept', + } + } + +The rules in `pre` should allow basic networking (such as ICMP and TCP), and ensure that existing connections are not closed. + +2. Add the `post` class to `my_fw/manifests/post.pp` and include any default rules to be applied last. + + class my_fw::post { + firewall { '999 drop all': + proto => 'all', + action => 'drop', + before => undef, + } + } + +####Create Firewall Rules + +The rules you create here are helpful if you don’t have any existing rules; they help you order your firewall configurations so you don’t lock yourself out of your box. + +Rules are persisted automatically between reboots, although there are known issues with ip6tables on older Debian/Ubuntu distributions. There are also known issues with ebtables. + +1. In `site.pp` or another top-scope file, add the following code to set up a metatype to purge unmanaged firewall resources. This will clear any existing rules and make sure that only rules defined in Puppet exist on the machine. + +**Note** - This only purges IPv4 rules. + + resources { "firewall": + purge => true + } + +2. Use the following code to set up the default parameters for all of the firewall rules you will establish later. These defaults will ensure that the `pre` and `post` classes are run in the correct order to avoid locking you out of your box during the first Puppet run. + + Firewall { + before => Class['my_fw::post'], + require => Class['my_fw::pre'], + } + +3. Then, declare the `my_fw::pre` and `my_fw::post` classes to satisfy dependencies. You can declare these classes using an **External Node Classifier** or the following code: + + class { ['my_fw::pre', 'my_fw::post']: } + +4. Include the `firewall` class to ensure the correct packages are installed. + + class { 'firewall': } + +###Upgrading + +Use these steps if you already have a version of the Firewall module installed. + +####From version 0.2.0 and more recent + +Upgrade the module with the puppet module tool as normal: + + puppet module upgrade puppetlabs/firewall + +##Usage + +There are two kinds of firewall rules you can use with Firewall: default rules and application-specific rules. Default rules apply to general firewall settings, whereas application-specific rules manage firewall settings for a specific application, node, etc. + +All rules employ a numbering system in the resource's title that is used for ordering. When titling your rules, make sure you prefix the rule with a number, for example, `000 accept all icmp requests` + + 000 runs first + 999 runs last + +###Default Rules + +You can place default rules in either `my_fw::pre` or `my_fw::post`, depending on when you would like them to run. Rules placed in the `pre` class will run first, and rules in the `post` class, last. + +In iptables, the title of the rule is stored using the comment feature of the underlying firewall subsystem. Values must match '/^\d+[[:alpha:][:digit:][:punct:][:space:]]+$/'. + +####Examples of Default Rules + +Basic accept ICMP request example: + + firewall { "000 accept all icmp requests": + proto => "icmp", + action => "accept", + } + +Drop all: + + firewall { "999 drop all other requests": + action => "drop", + } + +###Application-Specific Rules + +Puppet doesn't care where you define rules, and this means that you can place +your firewall resources as close to the applications and services that you +manage as you wish. If you use the [roles and profiles +pattern](https://puppetlabs.com/learn/roles-profiles-introduction) then it +makes sense to create your firewall rules in the profiles, so that they +remain close to the services managed by the profile. + +This is an example of firewall rules in a profile: + +```puppet +class profile::apache { + include apache + apache::vhost { 'mysite': ensure => present } + + firewall { '100 allow http and https access': + port => [80, 443], + proto => tcp, + action => accept, + } +} +``` + +###Rule inversion +Firewall rules may be inverted by prefixing the value of a parameter by "! ". If the value is an array, then every item in the array must be prefixed as iptables does not understand inverting a single value. + +Parameters that understand inversion are: connmark, ctstate, destination, dport, dst\_range, dst\_type, port, proto, source, sport, src\_range, src\_type, and state. + +Examples: + +```puppet +firewall { '001 disallow esp protocol': + action => 'accept', + proto => '! esp', +} +firewall { '002 drop NEW external website packets with FIN/RST/ACK set and SYN unset': + chain => 'INPUT', + state => 'NEW', + action => 'drop', + proto => 'tcp', + sport => ['! http', '! 443'], + source => '! 10.0.0.0/8', + tcp_flags => '! FIN,SYN,RST,ACK SYN', +} +``` + +###Additional Uses for the Firewall Module + +You can apply firewall rules to specific nodes. Usually, you will want to put the firewall rule in another class and apply that class to a node. Apply a rule to a node as follows: + + node 'some.node.com' { + firewall { '111 open port 111': + dport => 111 + } + } + +You can also do more complex things with the `firewall` resource. This example sets up static NAT for the source network 10.1.2.0/24: + + firewall { '100 snat for network foo2': + chain => 'POSTROUTING', + jump => 'MASQUERADE', + proto => 'all', + outiface => "eth0", + source => '10.1.2.0/24', + table => 'nat', + } + +The following example creates a new chain and forwards any port 5000 access to it. + + firewall { '100 forward to MY_CHAIN': + chain => 'INPUT', + jump => 'MY_CHAIN', + } + # The namevar here is in the format chain_name:table:protocol + firewallchain { 'MY_CHAIN:filter:IPv4': + ensure => present, + } + firewall { '100 my rule': + chain => 'MY_CHAIN', + action => 'accept', + proto => 'tcp', + dport => 5000, + } + +###Additional Information + +Access the inline documentation: + + puppet describe firewall + +Or + + puppet doc -r type + (and search for firewall) + +##Reference + +Classes: + +* [firewall](#class-firewall) + +Types: + +* [firewall](#type-firewall) +* [firewallchain](#type-firewallchain) + +Facts: + +* [ip6tables_version](#fact-ip6tablesversion) +* [iptables_version](#fact-iptablesversion) +* [iptables_persistent_version](#fact-iptablespersistentversion) + +###Class: firewall + +Performs the basic setup tasks required for using the firewall resources. + +At the moment this takes care of: + +* iptables-persistent package installation + +Include the `firewall` class for nodes that need to use the resources in this module: + + class { 'firewall': } + +####`ensure` + +Parameter that controls the state of the `iptables` service on your system, allowing you to disable `iptables` if you want. + +`ensure` can either be `running` or `stopped`. Default to `running`. + +###Type: firewall + +This type enables you to manage firewall rules within Puppet. + +#### Providers +**Note:** Not all features are available with all providers. + + * `ip6tables`: Ip6tables type provider + * Required binaries: `ip6tables-save`, `ip6tables`. + * Supported features: `connection_limiting`, `dnat`, `hop_limiting`, `icmp_match`, `interface_match`, `iptables`, `isfirstfrag`, `ishasmorefrags`, `islastfrag`, `log_level`, `log_prefix`, `mark`, `owner`, `pkttype`, `rate_limiting`, `recent_limiting`, `reject_type`, `snat`, `state_match`, `tcp_flags`. + +* `iptables`: Iptables type provider + * Required binaries: `iptables-save`, `iptables`. + * Default for `kernel` == `linux`. + * Supported features: `address_type`, `connection_limiting`, `dnat`, `icmp_match`, `interface_match`, `iprange`, `ipsec_dir`, `ipsec_policy`, `iptables`, `isfragment`, `log_level`, `log_prefix`, `mark`, `owner`, `pkttype`, `rate_limiting`, `recent_limiting`, `reject_type`, `snat`, `socket`, `state_match`, `tcp_flags`. + +**Autorequires:** + +If Puppet is managing the iptables or ip6tables chains specified in the `chain` or `jump` parameters, the firewall resource will autorequire those firewallchain resources. + +If Puppet is managing the iptables or iptables-persistent packages, and the provider is iptables or ip6tables, the firewall resource will autorequire those packages to ensure that any required binaries are installed. + +#### Features + +* `address_type`: The ability to match on source or destination address type. + +* `connection_limiting`: Connection limiting features. + +* `dnat`: Destination NATing. + +* `hop_limiting`: Hop limiting features. + +* `icmp_match`: The ability to match ICMP types. + +* `interface_match`: Interface matching. + +* `iprange`: The ability to match on source or destination IP range. + +* `ipsec_dir`: The ability to match IPsec policy direction. + +* `ipsec_policy`: The ability to match IPsec policy. + +* `iptables`: The provider provides iptables features. + +* `isfirstfrag`: The ability to match the first fragment of a fragmented ipv6 packet. + +* `isfragment`: The ability to match fragments. + +* `ishasmorefrags`: The ability to match a non-last fragment of a fragmented ipv6 packet. + +* `islastfrag`: The ability to match the last fragment of an ipv6 packet. + +* `log_level`: The ability to control the log level. + +* `log_prefix`: The ability to add prefixes to log messages. + +* `mark`: The ability to match or set the netfilter mark value associated with the packet. + +* `mask`: The ability to match recent rules based on the ipv4 mask. + +* `owner`: The ability to match owners. + +* `pkttype`: The ability to match a packet type. + +* `rate_limiting`: Rate limiting features. + +* `recent_limiting`: The netfilter recent module. + +* `reject_type`: The ability to control reject messages. + +* `snat`: Source NATing. + +* `socket`: The ability to match open sockets. + +* `state_match`: The ability to match stateful firewall states. + +* `tcp_flags`: The ability to match on particular TCP flag settings. + +#### Parameters + +* `action`: This is the action to perform on a match. Valid values for this action are: + * 'accept': The packet is accepted. + * 'reject': The packet is rejected with a suitable ICMP response. + * 'drop': The packet is dropped. + + If you specify no value it will simply match the rule but perform no action unless you provide a provider-specific parameter (such as `jump`). + +* `burst`: Rate limiting burst value (per second) before limit checks apply. Values must match '/^\d+$/'. Requires the `rate_limiting` feature. + +* `chain`: Name of the chain to use. You can provide a user-based chain or use one of the following built-in chains:'INPUT','FORWARD','OUTPUT','PREROUTING', or 'POSTROUTING'. The default value is 'INPUT'. Values must match '/^[a-zA-Z0-9\-_]+$/'. Requires the `iptables` feature. + +* `connlimit_above`: Connection limiting value for matched connections above n. Values must match '/^\d+$/'. Requires the `connection_limiting` feature. + +* `connlimit_mask`: Connection limiting by subnet mask for matched connections. Apply a subnet mask of /0 to /32 for IPv4, and a subnet mask of /0 to /128 for IPv6. Values must match '/^\d+$/'. Requires the `connection_limiting` feature. + +* `connmark`: Match the Netfilter mark value associated with the packet. Accepts values `mark/mask` or `mark`. These will be converted to hex if they are not hex already. Requires the `mark` feature. + +* `ctstate`: Matches a packet based on its state in the firewall stateful inspection table, using the conntrack module. Valid values are: 'INVALID', 'ESTABLISHED', 'NEW', 'RELATED'. Requires the `state_match` feature. + +* `destination`: The destination address to match. For example: `destination => '192.168.1.0/24'`. You can also negate a mask by putting ! in front. For example: `destination => '! 192.168.2.0/24'`. The destination can also be an IPv6 address if your provider supports it. + + For some firewall providers you can pass a range of ports in the format: 'start number-end number'. For example, '1-1024' would cover ports 1 to 1024. + +* `dport`: The destination port to match for this filter (if the protocol supports ports). Will accept a single element or an array. For some firewall providers you can pass a range of ports in the format: 'start number-end number'. For example, '1-1024' would cover ports 1 to 1024. + +* `dst_range`: The destination IP range. For example: `dst_range => '192.168.1.1-192.168.1.10'`. + + The destination IP range is must in 'IP1-IP2' format. Values must match '0.0.0.0-0.0.0.0' through '255.255.255.255-255.255.255.255'. Requires the `iprange` feature. + +* `dst_type`: The destination address type. For example: `dst_type => 'LOCAL'`. + + Valid values are: + + * 'UNSPEC': an unspecified address + * 'UNICAST': a unicast address + * 'LOCAL': a local address + * 'BROADCAST': a broadcast address + * 'ANYCAST': an anycast packet + * 'MULTICAST': a multicast address + * 'BLACKHOLE': a blackhole address + * 'UNREACHABLE': an unreachable address + * 'PROHIBIT': a prohibited address + * 'THROW': an unroutable address + * 'XRESOLVE: an unresolvable address + + Requires the `address_type` feature. + +* `ensure`: Ensures that the resource is present. Valid values are 'present', 'absent'. The default is 'present'. + +* `gid`: GID or Group owner matching rule. Accepts a string argument only, as iptables does not accept multiple gid in a single statement. Requires the `owner` feature. + +* `hop_limit`: Hop limiting value for matched packets. Values must match '/^\d+$/'. Requires the `hop_limiting` feature. + +* `icmp`: When matching ICMP packets, this indicates the type of ICMP packet to match. A value of 'any' is not supported. To match any type of ICMP packet, the parameter should be omitted or undefined. Requires the `icmp_match` feature. + +* `iniface`: Input interface to filter on. Values must match '/^[a-zA-Z0-9\-\._\+]+$/'. Requires the `interface_match` feature. + +* `ipsec_dir`: Sets the ipsec policy direction. Valid values are 'in', 'out'. Requires the `ipsec_dir` feature. + +* `ipsec_policy`: Sets the ipsec policy type. Valid values are 'none', 'ipsec'. Requires the `ipsec_policy` feature. + +* `isfirstfrag`: If true, matches when the packet is the first fragment of a fragmented ipv6 packet. Cannot be negated. Supported by ipv6 only. Valid values are 'true', 'false'. Requires the `isfirstfrag` feature. + +* `isfragment`: If 'true', matches when the packet is a tcp fragment of a fragmented packet. Supported by iptables only. Valid values are 'true', 'false'. Requires features `isfragment`. + +* `ishasmorefrags`: If 'true', matches when the packet has the 'more fragments' bit set. Supported by ipv6 only. Valid values are 'true', 'false'. Requires the `ishasmorefrags` feature. + +* `islastfrag`: If true, matches when the packet is the last fragment of a fragmented ipv6 packet. Supported by ipv6 only. Valid values are 'true', 'false'. Requires the `islastfrag`. + +* `jump`: The value for the iptables `--jump` parameter. Any valid chain name is allowed, but normal values are: 'QUEUE', 'RETURN', 'DNAT', 'SNAT', 'LOG', 'MASQUERADE', 'REDIRECT', 'MARK'. + + For the values 'ACCEPT', 'DROP', and 'REJECT', you must use the generic `action` parameter. This is to enforce the use of generic parameters where possible for maximum cross-platform modeling. + + If you set both `accept` and `jump` parameters, you will get an error, because only one of the options should be set. Requires the `iptables` feature. + +* `limit`: Rate limiting value for matched packets. The format is: 'rate/[/second/|/minute|/hour|/day]'. Example values are: '50/sec', '40/min', '30/hour', '10/day'. Requires the `rate_limiting` feature. + +* `line`: Read-only property for caching the rule line. + +* `log_level`: When combined with `jump => 'LOG'` specifies the system log level to log to. Requires the `log_level` feature. + +* `log_prefix`: When combined with `jump => 'LOG'` specifies the log prefix to use when logging. Requires the `log_prefix` feature. + +* `mask`: Sets the mask to use when `recent` is enabled. Requires the `mask` feature. + +* `name`: The canonical name of the rule. This name is also used for ordering, so make sure you prefix the rule with a number. For example: + +``` +firewall { '000 this runs first': + # this rule will run first +} +firewall { '999 this runs last': + # this rule will run last +} + ``` + + Depending on the provider, the name of the rule can be stored using the comment feature of the underlying firewall subsystem. Values must match '/^\d+[[:alpha:][:digit:][:punct:][:space:]]+$/'. + +* `outiface`: Output interface to filter on. Values must match '/^[a-zA-Z0-9\-\._\+]+$/'. Requires the `interface_match` feature. + +* `pkttype`: Sets the packet type to match. Valid values are: 'unicast', 'broadcast', and'multicast'. Requires the `pkttype` feature. + +* `port`: The destination or source port to match for this filter (if the protocol supports ports). Will accept a single element or an array. For some firewall providers you can pass a range of ports in the format: 'start number-end number'. For example, '1-1024' would cover ports 1 to 1024. + +* `proto`: The specific protocol to match for this rule. This is 'tcp' by default. Valid values are: + * 'tcp' + * 'udp' + * 'icmp' + * 'ipv6-icmp' + * 'esp' + * 'ah' + * 'vrrp' + * 'igmp' + * 'ipencap' + * 'ospf' + * 'gre' + * 'all' + +* `provider`: The specific backend to use for this firewall resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. Available providers are ip6tables and iptables. See the [Providers](#providers) section above for details about these providers. + + * `random`: When using a `jump` value of 'MASQUERADE', 'DNAT', 'REDIRECT', or 'SNAT', this boolean will enable randomized port mapping. Valid values are 'true', 'false'. Requires the `dnat` feature. + +* `rdest`: If boolean 'true', adds the destination IP address to the list. Valid values are 'true', 'false'. Requires the `recent_limiting` feature and the `recent` parameter. + +* `reap`: Can only be used in conjunction with the `rseconds` parameter. If boolean 'true', this will purge entries older than 'seconds' as specified in `rseconds`. Valid values are 'true', 'false'. Requires the `recent_limiting` feature and the `recent` parameter. + +* `recent`: Enable the recent module. Valid values are: 'set', 'update', 'rcheck', or 'remove'. For example: + +``` +# If anyone's appeared on the 'badguy' blacklist within +# the last 60 seconds, drop their traffic, and update the timestamp. +firewall { '100 Drop badguy traffic': + recent => 'update', + rseconds => 60, + rsource => true, + rname => 'badguy', + action => 'DROP', + chain => 'FORWARD', +} +# No-one should be sending us traffic on eth0 from localhost +# Blacklist them +firewall { '101 blacklist strange traffic': + recent => 'set', + rsource => true, + rname => 'badguy', + destination => '127.0.0.0/8', + iniface => 'eth0', + action => 'DROP', + chain => 'FORWARD', +} +``` + + Requires the `recent_limiting` feature. + +* `reject`: When combined with `jump => 'REJECT'`, you can specify a different ICMP response to be sent back to the packet sender. Requires the `reject_type` feature. + +* `rhitcount`: Used in conjunction with `recent => 'update'` or `recent => 'rcheck'`. When used, this will narrow the match to happen only when the address is in the list and packets greater than or equal to the given value have been received. Requires the `recent_limiting` feature and the `recent` parameter. + +* `rname`: Specify the name of the list. Takes a string argument. Requires the `recent_limiting` feature and the `recent` parameter. + +* `rseconds`: Used in conjunction with `recent => 'rcheck'` or `recent => 'update'`. When used, this will narrow the match to only happen when the address is in the list and was seen within the last given number of seconds. Requires the `recent_limiting` feature and the `recent` parameter. + +* `rsource`: If boolean 'true', adds the source IP address to the list. Valid values are 'true', 'false'. Requires the `recent_limiting` feature and the `recent` parameter. + +* `rttl`: May only be used in conjunction with `recent => 'rcheck'` or `recent => 'update'`. If boolean 'true', this will narrow the match to happen only when the address is in the list and the TTL of the current packet matches that of the packet that hit the `recent => 'set'` rule. If you have problems with DoS attacks via bogus packets from fake source addresses, this parameter may help. Valid values are 'true', 'false'. Requires the `recent_limiting` feature and the `recent` parameter. + +* `set_mark`: Set the Netfilter mark value associated with the packet. Accepts either 'mark/mask' or 'mark'. These will be converted to hex if they are not already. Requires the `mark` feature. + +* `socket`: If 'true', matches if an open socket can be found by doing a socket lookup on the packet. Valid values are 'true', 'false'. Requires the `socket` feature. + +* `source`: The source address. For example: `source => '192.168.2.0/24'`. You can also negate a mask by putting ! in front. For example: `source => '! 192.168.2.0/24'`. The source can also be an IPv6 address if your provider supports it. + +* `sport`: The source port to match for this filter (if the protocol supports ports). Will accept a single element or an array. For some firewall providers you can pass a range of ports in the format:'start number-end number'. For example, '1-1024' would cover ports 1 to 1024. + +* `src_range`: The source IP range. For example: `src_range => '192.168.1.1-192.168.1.10'`. The source IP range is must in 'IP1-IP2' format. Values must match '0.0.0.0-0.0.0.0' through '255.255.255.255-255.255.255.255'. Requires the `iprange` feature. + +* `src_type`: Specify the source address type. For example: `src_type => 'LOCAL'`. + + Valid values are: + + * 'UNSPEC': an unspecified address. + * 'UNICAST': a unicast address. + * 'LOCAL': a local address. + * 'BROADCAST': a broadcast address. + * 'ANYCAST': an anycast packet. + * 'MULTICAST': a multicast address. + * 'BLACKHOLE': a blackhole address. + * 'UNREACHABLE': an unreachable address. + * 'PROHIBIT': a prohibited address. + * 'THROW': an unroutable address. + * 'XRESOLVE': an unresolvable address. + + Requires the `address_type` feature. + +* `stat_every`: Match one packet every nth packet. Requires `stat_mode => 'nth'` + +* `stat_mode`: Set the matching mode for statistic matching. Supported modes are `random` and `nth`. + +* `stat_packet`: Set the initial counter value for the nth mode. Must be between 0 and the value of `stat_every`. Defaults to 0. Requires `stat_mode => 'nth'` + +* `stat_probability`: Set the probability from 0 to 1 for a packet to be randomly matched. It works only with `stat_mode => 'random'`. + +* `state`: Matches a packet based on its state in the firewall stateful inspection table. Valid values are: 'INVALID', 'ESTABLISHED', 'NEW', 'RELATED'. Requires the `state_match` feature. + +* `table`: Table to use. Valid values are: 'nat', 'mangle', 'filter', 'raw', 'rawpost'. By default the setting is 'filter'. Requires the `iptables` feature. + +* `tcp_flags`: Match when the TCP flags are as specified. Set as a string with a list of comma-separated flag names for the mask, then a space, then a comma-separated list of flags that should be set. The flags are: 'SYN', 'ACK', 'FIN', 'RST', 'URG', 'PSH', 'ALL', 'NONE'. + + Note that you specify flags in the order that iptables `--list` rules would list them to avoid having Puppet think you changed the flags. For example, 'FIN,SYN,RST,ACK SYN' matches packets with the SYN bit set and the ACK, RST and FIN bits cleared. Such packets are used to request TCP connection initiation. Requires the `tcp_flags` feature. + +* `todest`: When using `jump => 'DNAT'`, you can specify the new destination address using this parameter. Requires the `dnat` feature. + +* `toports`: For DNAT this is the port that will replace the destination port. Requires the `dnat` feature. + +* `tosource`: When using `jump => 'SNAT'`, you can specify the new source address using this parameter. Requires the `snat` feature. + +* `uid`: UID or Username owner matching rule. Accepts a string argument only, as iptables does not accept multiple uid in a single statement. Requires the `owner` feature. + +###Type: firewallchain + +Enables you to manage rule chains for firewalls. + +Currently this type supports only iptables, ip6tables, and ebtables on Linux. It also provides support for setting the default policy on chains and tables that allow it. + +**Autorequires**: If Puppet is managing the iptables or iptables-persistent packages, and the provider is iptables_chain, the firewall resource will autorequire those packages to ensure that any required binaries are installed. + +####Providers + +`iptables_chain` is the only provider that supports firewallchain. + +####Features + +* `iptables_chain`: The provider provides iptables chain features. +* `policy`: Default policy (inbuilt chains only). + +####Parameters + +* `ensure`: Ensures that the resource is present. Valid values are 'present', 'absent'. + +* `ignore`: Regex to perform on firewall rules to exempt unmanaged rules from purging (when enabled). This is matched against the output of iptables-save. This can be a single regex or an array of them. To support flags, use the ruby inline flag mechanism: a regex such as `/foo/i` can be written as `(?i)foo` or `(?i:foo)`. Only when purge is 'true'. + + Full example: + + firewallchain { 'INPUT:filter:IPv4': + purge => true, + ignore => [ + # ignore the fail2ban jump rule + '-j fail2ban-ssh', + # ignore any rules with "ignore" (case insensitive) in the comment in the rule + '--comment "[^"](?i:ignore)[^"]"', + ], + } + +* `name`: Specify the canonical name of the chain. For iptables the format must be {chain}:{table}:{protocol}. + +* `policy`: Set the action the packet will perform when the end of the chain is reached. It can only be set on inbuilt chains ('INPUT', 'FORWARD', 'OUTPUT', 'PREROUTING', 'POSTROUTING'). Valid values are: + + * 'accept': The packet is accepted. + * 'drop': The packet is dropped. + * 'queue': The packet is passed userspace. + * 'return': The packet is returned to calling (jump) queue or to the default of inbuilt chains. + +* `provider`: The specific backend to use for this firewallchain resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. The only available provider is: + + `iptables_chain`: iptables chain provider + + * Required binaries: `ebtables-save`, `ebtables`, `ip6tables-save`, `ip6tables`, `iptables-save`, `iptables`. + * Default for `kernel` == `linux`. + * Supported features: `iptables_chain`, `policy`. + +* `purge`: Purge unmanaged firewall rules in this chain. Valid values are 'false', 'true'. + +###Fact: ip6tables_version + +A Facter fact that can be used to determine what the default version of ip6tables is for your operating system/distribution. + +###Fact: iptables_version + +A Facter fact that can be used to determine what the default version of iptables is for your operating system/distribution. + +###Fact: iptables_persistent_version + +Retrieves the version of iptables-persistent from your OS. This is a Debian/Ubuntu specific fact. + +##Limitations + +###SLES + +The `socket` parameter is not supported on SLES. In this release it will cause +the catalog to fail with iptables failures, rather than correctly warn you that +the features are unusable. + +###Oracle Enterprise Linux + +The `socket` and `owner` parameters are unsupported on Oracle Enterprise Linux +when the "Unbreakable" kernel is used. These may function correctly when using +the stock RedHat kernel instead. Declaring either of these parameters on an +unsupported system will result in iptable rules failing to apply. + +###Other + +Bugs can be reported using JIRA issues + + + +##Development + +Puppet Labs modules on the Puppet Forge are open projects, and community contributions are essential for keeping them great. We can’t access the huge number of platforms and myriad of hardware, software, and deployment configurations that Puppet is intended to serve. + +We want to keep it as easy as possible to contribute changes so that our modules work in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. + +You can read the complete module contribution guide [on the Puppet Labs wiki.](http://projects.puppetlabs.com/projects/module-site/wiki/Module_contributing) + +For this particular module, please also read CONTRIBUTING.md before contributing. + +Currently we support: + +* iptables +* ip6tables +* ebtables (chains only) + +###Testing + +Make sure you have: + +* rake +* bundler + +Install the necessary gems: + + bundle install + +And run the tests from the root of the source code: + + rake test + +If you have a copy of Vagrant 1.1.0 you can also run the system tests: + + RS_SET=ubuntu-1404-x64 rspec spec/acceptance + RS_SET=centos-64-x64 rspec spec/acceptance + + + diff --git a/firewall/Rakefile b/firewall/Rakefile new file mode 100644 index 000000000..5868545f2 --- /dev/null +++ b/firewall/Rakefile @@ -0,0 +1,10 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_inherits_from_params_class') +PuppetLint.configuration.send('disable_class_parameter_defaults') +PuppetLint.configuration.send('disable_documentation') +PuppetLint.configuration.send('disable_single_quote_string_with_variables') +PuppetLint.configuration.ignore_paths = ["spec/**/*.pp", "pkg/**/*.pp"] diff --git a/firewall/lib/facter/ip6tables_version.rb b/firewall/lib/facter/ip6tables_version.rb new file mode 100644 index 000000000..3dce27f70 --- /dev/null +++ b/firewall/lib/facter/ip6tables_version.rb @@ -0,0 +1,11 @@ +Facter.add(:ip6tables_version) do + confine :kernel => :linux + setcode do + version = Facter::Util::Resolution.exec('ip6tables --version') + if version + version.match(/\d+\.\d+\.\d+/).to_s + else + nil + end + end +end diff --git a/firewall/lib/facter/iptables_persistent_version.rb b/firewall/lib/facter/iptables_persistent_version.rb new file mode 100644 index 000000000..80bf9dea1 --- /dev/null +++ b/firewall/lib/facter/iptables_persistent_version.rb @@ -0,0 +1,15 @@ +Facter.add(:iptables_persistent_version) do + confine :operatingsystem => %w{Debian Ubuntu} + setcode do + # Throw away STDERR because dpkg >= 1.16.7 will make some noise if the + # package isn't currently installed. + cmd = "dpkg-query -Wf '${Version}' iptables-persistent 2>/dev/null" + version = Facter::Util::Resolution.exec(cmd) + + if version.nil? or !version.match(/\d+\.\d+/) + nil + else + version + end + end +end diff --git a/firewall/lib/facter/iptables_version.rb b/firewall/lib/facter/iptables_version.rb new file mode 100644 index 000000000..6f7ae5647 --- /dev/null +++ b/firewall/lib/facter/iptables_version.rb @@ -0,0 +1,11 @@ +Facter.add(:iptables_version) do + confine :kernel => :linux + setcode do + version = Facter::Util::Resolution.exec('iptables --version') + if version + version.match(/\d+\.\d+\.\d+/).to_s + else + nil + end + end +end diff --git a/firewall/lib/puppet/provider/firewall.rb b/firewall/lib/puppet/provider/firewall.rb new file mode 100644 index 000000000..c6b0b10bb --- /dev/null +++ b/firewall/lib/puppet/provider/firewall.rb @@ -0,0 +1,34 @@ +class Puppet::Provider::Firewall < Puppet::Provider + + # Prefetch our rule list. This is ran once every time before any other + # action (besides initialization of each object). + def self.prefetch(resources) + debug("[prefetch(resources)]") + instances.each do |prov| + if resource = resources[prov.name] || resources[prov.name.downcase] + resource.provider = prov + end + end + end + + # Look up the current status. This allows us to conventiently look up + # existing status with properties[:foo]. + def properties + if @property_hash.empty? + @property_hash = query || {:ensure => :absent} + @property_hash[:ensure] = :absent if @property_hash.empty? + end + @property_hash.dup + end + + # Pull the current state of the list from the full list. We're + # getting some double entendre here.... + def query + self.class.instances.each do |instance| + if instance.name == self.name or instance.name.downcase == self.name + return instance.properties + end + end + nil + end +end diff --git a/firewall/lib/puppet/provider/firewall/ip6tables.rb b/firewall/lib/puppet/provider/firewall/ip6tables.rb new file mode 100644 index 000000000..2ed90a8fc --- /dev/null +++ b/firewall/lib/puppet/provider/firewall/ip6tables.rb @@ -0,0 +1,142 @@ +Puppet::Type.type(:firewall).provide :ip6tables, :parent => :iptables, :source => :iptables do + @doc = "Ip6tables type provider" + + has_feature :iptables + has_feature :connection_limiting + has_feature :hop_limiting + has_feature :rate_limiting + has_feature :recent_limiting + has_feature :snat + has_feature :dnat + has_feature :interface_match + has_feature :icmp_match + has_feature :owner + has_feature :state_match + has_feature :reject_type + has_feature :log_level + has_feature :log_prefix + has_feature :mark + has_feature :tcp_flags + has_feature :pkttype + has_feature :ishasmorefrags + has_feature :islastfrag + has_feature :isfirstfrag + + optional_commands({ + :ip6tables => 'ip6tables', + :ip6tables_save => 'ip6tables-save', + }) + + confine :kernel => :linux + + def initialize(*args) + if Facter.fact('ip6tables_version').value.match /1\.3\.\d/ + raise ArgumentError, 'The ip6tables provider is not supported on version 1.3 of iptables' + else + super + end + end + + def self.iptables(*args) + ip6tables(*args) + end + + def self.iptables_save(*args) + ip6tables_save(*args) + end + + @protocol = "IPv6" + + @resource_map = { + :burst => "--limit-burst", + :connlimit_above => "-m connlimit --connlimit-above", + :connlimit_mask => "--connlimit-mask", + :connmark => "-m connmark --mark", + :ctstate => "-m conntrack --ctstate", + :destination => "-d", + :dport => "-m multiport --dports", + :gid => "-m owner --gid-owner", + :hop_limit => "-m hl --hl-eq", + :icmp => "-m icmp6 --icmpv6-type", + :iniface => "-i", + :isfirstfrag => "-m frag --fragid 0 --fragfirst", + :ishasmorefrags => "-m frag --fragid 0 --fragmore", + :islastfrag => "-m frag --fragid 0 --fraglast", + :jump => "-j", + :limit => "-m limit --limit", + :log_level => "--log-level", + :log_prefix => "--log-prefix", + :name => "-m comment --comment", + :outiface => "-o", + :pkttype => "-m pkttype --pkt-type", + :port => '-m multiport --ports', + :proto => "-p", + :rdest => "--rdest", + :reap => "--reap", + :recent => "-m recent", + :reject => "--reject-with", + :rhitcount => "--hitcount", + :rname => "--name", + :rseconds => "--seconds", + :rsource => "--rsource", + :rttl => "--rttl", + :source => "-s", + :sport => "-m multiport --sports", + :stat_every => '--every', + :stat_mode => "-m statistic --mode", + :stat_packet => '--packet', + :stat_probability => '--probability', + :state => "-m state --state", + :table => "-t", + :todest => "--to-destination", + :toports => "--to-ports", + :tosource => "--to-source", + :uid => "-m owner --uid-owner", + } + + # These are known booleans that do not take a value, but we want to munge + # to true if they exist. + @known_booleans = [:ishasmorefrags, :islastfrag, :isfirstfrag, :rsource, :rdest, :reap, :rttl] + + # Create property methods dynamically + (@resource_map.keys << :chain << :table << :action).each do |property| + if @known_booleans.include?(property) then + # The boolean properties default to '' which should be read as false + define_method "#{property}" do + @property_hash[property] = :false if @property_hash[property] == nil + @property_hash[property.to_sym] + end + else + define_method "#{property}" do + @property_hash[property.to_sym] + end + end + + if property == :chain + define_method "#{property}=" do |value| + if @property_hash[:chain] != value + raise ArgumentError, "Modifying the chain for existing rules is not supported." + end + end + else + define_method "#{property}=" do |value| + @property_hash[:needs_change] = true + end + end + end + + # This is the order of resources as they appear in iptables-save output, + # we need it to properly parse and apply rules, if the order of resource + # changes between puppet runs, the changed rules will be re-applied again. + # This order can be determined by going through iptables source code or just tweaking and trying manually + # (Note: on my CentOS 6.4 ip6tables-save returns -m frag on the place + # I put it when calling the command. So compability with manual changes + # not provided with current parser [georg.koester]) + @resource_list = [:table, :source, :destination, :iniface, :outiface, + :proto, :ishasmorefrags, :islastfrag, :isfirstfrag, :gid, :uid, :sport, :dport, + :port, :pkttype, :name, :state, :ctstate, :icmp, :hop_limit, :limit, :burst, + :recent, :rseconds, :reap, :rhitcount, :rttl, :rname, :rsource, :rdest, + :jump, :todest, :tosource, :toports, :log_level, :log_prefix, :reject, + :connlimit_above, :connlimit_mask, :connmark] + +end diff --git a/firewall/lib/puppet/provider/firewall/iptables.rb b/firewall/lib/puppet/provider/firewall/iptables.rb new file mode 100644 index 000000000..09816a3a2 --- /dev/null +++ b/firewall/lib/puppet/provider/firewall/iptables.rb @@ -0,0 +1,574 @@ +require 'puppet/provider/firewall' +require 'digest/md5' + +Puppet::Type.type(:firewall).provide :iptables, :parent => Puppet::Provider::Firewall do + include Puppet::Util::Firewall + + @doc = "Iptables type provider" + + has_feature :iptables + has_feature :connection_limiting + has_feature :rate_limiting + has_feature :recent_limiting + has_feature :snat + has_feature :dnat + has_feature :interface_match + has_feature :icmp_match + has_feature :owner + has_feature :state_match + has_feature :reject_type + has_feature :log_level + has_feature :log_prefix + has_feature :mark + has_feature :tcp_flags + has_feature :pkttype + has_feature :isfragment + has_feature :socket + has_feature :address_type + has_feature :iprange + has_feature :ipsec_dir + has_feature :ipsec_policy + has_feature :mask + + optional_commands({ + :iptables => 'iptables', + :iptables_save => 'iptables-save', + }) + + defaultfor :kernel => :linux + confine :kernel => :linux + + iptables_version = Facter.fact('iptables_version').value + if (iptables_version and Puppet::Util::Package.versioncmp(iptables_version, '1.4.1') < 0) + mark_flag = '--set-mark' + else + mark_flag = '--set-xmark' + end + + @protocol = "IPv4" + + @resource_map = { + :burst => "--limit-burst", + :connlimit_above => "-m connlimit --connlimit-above", + :connlimit_mask => "--connlimit-mask", + :connmark => "-m connmark --mark", + :ctstate => "-m conntrack --ctstate", + :destination => "-d", + :dport => ["-m multiport --dports", "--dport"], + :dst_range => "-m iprange --dst-range", + :dst_type => "-m addrtype --dst-type", + :gid => "-m owner --gid-owner", + :icmp => "-m icmp --icmp-type", + :iniface => "-i", + :ipsec_dir => "-m policy --dir", + :ipsec_policy => "--pol", + :isfragment => "-f", + :jump => "-j", + :limit => "-m limit --limit", + :log_level => "--log-level", + :log_prefix => "--log-prefix", + :mac_source => ["-m mac --mac-source", "--mac-source"], + :mask => '--mask', + :name => "-m comment --comment", + :outiface => "-o", + :pkttype => "-m pkttype --pkt-type", + :port => '-m multiport --ports', + :proto => "-p", + :random => "--random", + :rdest => "--rdest", + :reap => "--reap", + :recent => "-m recent", + :reject => "--reject-with", + :rhitcount => "--hitcount", + :rname => "--name", + :rseconds => "--seconds", + :rsource => "--rsource", + :rttl => "--rttl", + :set_mark => mark_flag, + :socket => "-m socket", + :source => "-s", + :sport => ["-m multiport --sports", "--sport"], + :src_range => "-m iprange --src-range", + :src_type => "-m addrtype --src-type", + :stat_every => '--every', + :stat_mode => "-m statistic --mode", + :stat_packet => '--packet', + :stat_probability => '--probability', + :state => "-m state --state", + :table => "-t", + :tcp_flags => "-m tcp --tcp-flags", + :todest => "--to-destination", + :toports => "--to-ports", + :tosource => "--to-source", + :uid => "-m owner --uid-owner", + } + + # These are known booleans that do not take a value, but we want to munge + # to true if they exist. + @known_booleans = [ + :isfragment, + :random, + :rdest, + :reap, + :rsource, + :rttl, + :socket + ] + + + # Create property methods dynamically + (@resource_map.keys << :chain << :table << :action).each do |property| + if @known_booleans.include?(property) then + # The boolean properties default to '' which should be read as false + define_method "#{property}" do + @property_hash[property] = :false if @property_hash[property] == nil + @property_hash[property.to_sym] + end + else + define_method "#{property}" do + @property_hash[property.to_sym] + end + end + + if property == :chain + define_method "#{property}=" do |value| + if @property_hash[:chain] != value + raise ArgumentError, "Modifying the chain for existing rules is not supported." + end + end + else + define_method "#{property}=" do |value| + @property_hash[:needs_change] = true + end + end + end + + # This is the order of resources as they appear in iptables-save output, + # we need it to properly parse and apply rules, if the order of resource + # changes between puppet runs, the changed rules will be re-applied again. + # This order can be determined by going through iptables source code or just tweaking and trying manually + @resource_list = [ + :table, :source, :destination, :iniface, :outiface, :proto, :isfragment, + :stat_mode, :stat_every, :stat_packet, :stat_probability, + :src_range, :dst_range, :tcp_flags, :gid, :uid, :mac_source, :sport, :dport, :port, + :dst_type, :src_type, :socket, :pkttype, :name, :ipsec_dir, :ipsec_policy, + :state, :ctstate, :icmp, :limit, :burst, :recent, :rseconds, :reap, + :rhitcount, :rttl, :rname, :mask, :rsource, :rdest, :jump, :todest, + :tosource, :toports, :random, :log_prefix, :log_level, :reject, :set_mark, + :connlimit_above, :connlimit_mask, :connmark + ] + + def insert + debug 'Inserting rule %s' % resource[:name] + iptables insert_args + end + + def update + debug 'Updating rule %s' % resource[:name] + iptables update_args + end + + def delete + debug 'Deleting rule %s' % resource[:name] + iptables delete_args + end + + def exists? + properties[:ensure] != :absent + end + + # Flush the property hash once done. + def flush + debug("[flush]") + if @property_hash.delete(:needs_change) + notice("Properties changed - updating rule") + update + end + persist_iptables(self.class.instance_variable_get(:@protocol)) + @property_hash.clear + end + + def self.instances + debug "[instances]" + table = nil + rules = [] + counter = 1 + + # String#lines would be nice, but we need to support Ruby 1.8.5 + iptables_save.split("\n").each do |line| + unless line =~ /^\#\s+|^\:\S+|^COMMIT|^FATAL/ + if line =~ /^\*/ + table = line.sub(/\*/, "") + else + if hash = rule_to_hash(line, table, counter) + rules << new(hash) + counter += 1 + end + end + end + end + rules + end + + def self.rule_to_hash(line, table, counter) + hash = {} + keys = [] + values = line.dup + + #################### + # PRE-PARSE CLUDGING + #################### + + # --tcp-flags takes two values; we cheat by adding " around it + # so it behaves like --comment + values = values.gsub(/(!\s+)?--tcp-flags (\S*) (\S*)/, '--tcp-flags "\1\2 \3"') + # we do a similar thing for negated address masks (source and destination). + values = values.gsub(/(-\S+) (!)\s?(\S*)/,'\1 "\2 \3"') + # the actual rule will have the ! mark before the option. + values = values.gsub(/(!)\s*(-\S+)\s*(\S*)/, '\2 "\1 \3"') + # The match extension for tcp & udp are optional and throws off the @resource_map. + values = values.gsub(/-m (tcp|udp) (--(s|d)port|-m multiport)/, '\2') + # '--pol ipsec' takes many optional arguments; we cheat again by adding " around them + values = values.sub(/ + --pol\sipsec + (\s--strict)? + (\s--reqid\s\S+)? + (\s--spi\s\S+)? + (\s--proto\s\S+)? + (\s--mode\s\S+)? + (\s--tunnel-dst\s\S+)? + (\s--tunnel-src\s\S+)? + (\s--next)?/x, + '--pol "ipsec\1\2\3\4\5\6\7\8" ' + ) + + # Trick the system for booleans + @known_booleans.each do |bool| + # append "true" because all params are expected to have values + if bool == :isfragment then + # -f requires special matching: + # only replace those -f that are not followed by an l to + # distinguish between -f and the '-f' inside of --tcp-flags. + values = values.sub(/-f(?!l)(?=.*--comment)/, '-f true') + else + values = values.sub(/#{@resource_map[bool]}/, "#{@resource_map[bool]} true") + end + end + + ############ + # Populate parser_list with used value, in the correct order + ############ + map_index={} + @resource_map.each_pair do |map_k,map_v| + [map_v].flatten.each do |v| + ind=values.index(/\s#{v}/) + next unless ind + map_index[map_k]=ind + end + end + # Generate parser_list based on the index of the found option + parser_list=[] + map_index.sort_by{|k,v| v}.each{|mapi| parser_list << mapi.first } + + ############ + # MAIN PARSE + ############ + + # Here we iterate across our values to generate an array of keys + parser_list.reverse.each do |k| + resource_map_key = @resource_map[k] + [resource_map_key].flatten.each do |opt| + if values.slice!(/\s#{opt}/) + keys << k + break + end + end + end + + # Manually remove chain + values.slice!('-A') + keys << :chain + + # Here we generate the main hash + keys.zip(values.scan(/"[^"]*"|\S+/).reverse) { |f, v| hash[f] = v.gsub(/"/, '') } + + ##################### + # POST PARSE CLUDGING + ##################### + + [:dport, :sport, :port, :state, :ctstate].each do |prop| + hash[prop] = hash[prop].split(',') if ! hash[prop].nil? + end + + # Convert booleans removing the previous cludge we did + @known_booleans.each do |bool| + if hash[bool] != nil then + if hash[bool] != "true" then + raise "Parser error: #{bool} was meant to be a boolean but received value: #{hash[bool]}." + end + end + end + + # Our type prefers hyphens over colons for ranges so ... + # Iterate across all ports replacing colons with hyphens so that ranges match + # the types expectations. + [:dport, :sport, :port].each do |prop| + next unless hash[prop] + hash[prop] = hash[prop].collect do |elem| + elem.gsub(/:/,'-') + end + end + + # Invert any rules that are prefixed with a '!' + [ + :connmark, + :ctstate, + :destination, + :dport, + :dst_range, + :dst_type, + :port, + :proto, + :source, + :sport, + :src_range, + :src_type, + :state, + ].each do |prop| + if hash[prop] and hash[prop].is_a?(Array) + # find if any are negated, then negate all if so + should_negate = hash[prop].index do |value| + value.match(/^(!)\s+/) + end + hash[prop] = hash[prop].collect { |v| + "! #{v.sub(/^!\s+/,'')}" + } if should_negate + elsif hash[prop] + m = hash[prop].match(/^(!?)\s?(.*)/) + neg = "! " if m[1] == "!" + if [:source,:destination].include?(prop) + # Normalise all rules to CIDR notation. + hash[prop] = "#{neg}#{Puppet::Util::IPCidr.new(m[2]).cidr}" + else + hash[prop] = "#{neg}#{m[2]}" + end + end + end + + # States should always be sorted. This ensures that the output from + # iptables-save and user supplied resources is consistent. + hash[:state] = hash[:state].sort unless hash[:state].nil? + hash[:ctstate] = hash[:ctstate].sort unless hash[:ctstate].nil? + + # This forces all existing, commentless rules or rules with invalid comments to be moved + # to the bottom of the stack. + # Puppet-firewall requires that all rules have comments (resource names) and match this + # regex and will fail if a rule in iptables does not have a comment. We get around this + # by appending a high level + if ! hash[:name] + num = 9000 + counter + hash[:name] = "#{num} #{Digest::MD5.hexdigest(line)}" + elsif not /^\d+[[:alpha:][:digit:][:punct:][:space:]]+$/ =~ hash[:name] + num = 9000 + counter + hash[:name] = "#{num} #{/([[:alpha:][:digit:][:punct:][:space:]]+)/.match(hash[:name])[1]}" + end + + # Iptables defaults to log_level '4', so it is omitted from the output of iptables-save. + # If the :jump value is LOG and you don't have a log-level set, we assume it to be '4'. + if hash[:jump] == 'LOG' && ! hash[:log_level] + hash[:log_level] = '4' + end + + # Iptables defaults to burst '5', so it is ommitted from the output of iptables-save. + # If the :limit value is set and you don't have a burst set, we assume it to be '5'. + if hash[:limit] && ! hash[:burst] + hash[:burst] = '5' + end + + hash[:line] = line + hash[:provider] = self.name.to_s + hash[:table] = table + hash[:ensure] = :present + + # Munge some vars here ... + + # Proto should equal 'all' if undefined + hash[:proto] = "all" if !hash.include?(:proto) + + # If the jump parameter is set to one of: ACCEPT, REJECT or DROP then + # we should set the action parameter instead. + if ['ACCEPT','REJECT','DROP'].include?(hash[:jump]) then + hash[:action] = hash[:jump].downcase + hash.delete(:jump) + end + + hash + end + + def insert_args + args = [] + args << ["-I", resource[:chain], insert_order] + args << general_args + args + end + + def update_args + args = [] + args << ["-R", resource[:chain], insert_order] + args << general_args + args + end + + def delete_args + # Split into arguments + line = properties[:line].gsub(/\-A/, '-D').split(/\s(?=(?:[^"]|"[^"]*")*$)/).map{|v| v.gsub(/"/, '')} + line.unshift("-t", properties[:table]) + end + + # This method takes the resource, and attempts to generate the command line + # arguments for iptables. + def general_args + debug "Current resource: %s" % resource.class + + args = [] + resource_list = self.class.instance_variable_get('@resource_list') + resource_map = self.class.instance_variable_get('@resource_map') + known_booleans = self.class.instance_variable_get('@known_booleans') + + resource_list.each do |res| + resource_value = nil + if (resource[res]) then + resource_value = resource[res] + # If socket is true then do not add the value as -m socket is standalone + if known_booleans.include?(res) then + if resource[res] == :true then + resource_value = nil + else + # If the property is not :true then we don't want to add the value + # to the args list + next + end + end + elsif res == :jump and resource[:action] then + # In this case, we are substituting jump for action + resource_value = resource[:action].to_s.upcase + else + next + end + + args << [resource_map[res]].flatten.first.split(' ') + args = args.flatten + + # On negations, the '!' has to be before the option (eg: "! -d 1.2.3.4") + if resource_value.is_a?(String) and resource_value.sub!(/^!\s*/, '') then + # we do this after adding the 'dash' argument because of ones like "-m multiport --dports", where we want it before the "--dports" but after "-m multiport". + # so we insert before whatever the last argument is + args.insert(-2, '!') + elsif resource_value.is_a?(Symbol) and resource_value.to_s.match(/^!/) then + #ruby 1.8.7 can't .match Symbols ------------------ ^ + resource_value = resource_value.to_s.sub!(/^!\s*/, '').to_sym + args.insert(-2, '!') + elsif resource_value.is_a?(Array) + should_negate = resource_value.index do |value| + #ruby 1.8.7 can't .match symbols + value.to_s.match(/^(!)\s+/) + end + if should_negate + resource_value, wrong_values = resource_value.collect do |value| + if value.is_a?(String) + wrong = value if ! value.match(/^!\s+/) + [value.sub(/^!\s*/, ''),wrong] + else + [value,nil] + end + end.transpose + wrong_values = wrong_values.compact + if ! wrong_values.empty? + fail "All values of the '#{res}' property must be prefixed with a '!' when inverting, but '#{wrong_values.join("', '")}' #{wrong_values.length>1?"are":"is"} not prefixed; aborting" + end + args.insert(-2, '!') + end + end + + + # For sport and dport, convert hyphens to colons since the type + # expects hyphens for ranges of ports. + if [:sport, :dport, :port].include?(res) then + resource_value = resource_value.collect do |elem| + elem.gsub(/-/, ':') + end + end + + # our tcp_flags takes a single string with comma lists separated + # by space + # --tcp-flags expects two arguments + if res == :tcp_flags + one, two = resource_value.split(' ') + args << one + args << two + elsif resource_value.is_a?(Array) + args << resource_value.join(',') + elsif !resource_value.nil? + args << resource_value + end + end + + args + end + + def insert_order + debug("[insert_order]") + rules = [] + + # Find list of current rules based on chain and table + self.class.instances.each do |rule| + if rule.chain == resource[:chain].to_s and rule.table == resource[:table].to_s + rules << rule.name + end + end + + # No rules at all? Just bail now. + return 1 if rules.empty? + + # Add our rule to the end of the array of known rules + my_rule = resource[:name].to_s + rules << my_rule + + unmanaged_rule_regex = /^9[0-9]{3}\s[a-f0-9]{32}$/ + # Find if this is a new rule or an existing rule, then find how many + # unmanaged rules preceed it. + if rules.length == rules.uniq.length + # This is a new rule so find its ordered location. + new_rule_location = rules.sort.uniq.index(my_rule) + if new_rule_location == 0 + # The rule will be the first rule in the chain because nothing came + # before it. + offset_rule = rules[0] + else + # This rule will come after other managed rules, so find the rule + # immediately preceeding it. + offset_rule = rules.sort.uniq[new_rule_location - 1] + end + else + # This is a pre-existing rule, so find the offset from the original + # ordering. + offset_rule = my_rule + end + # Count how many unmanaged rules are ahead of the target rule so we know + # how much to add to the insert order + unnamed_offset = rules[0..rules.index(offset_rule)].inject(0) do |sum,rule| + # This regex matches the names given to unmanaged rules (a number + # 9000-9999 followed by an MD5 hash). + sum + (rule.match(unmanaged_rule_regex) ? 1 : 0) + end + + # We want our rule to come before unmanaged rules if it's not a 9-rule + if offset_rule.match(unmanaged_rule_regex) and ! my_rule.match(/^9/) + unnamed_offset -= 1 + end + + # Insert our new or updated rule in the correct order of named rules, but + # offset for unnamed rules. + rules.reject{|r|r.match(unmanaged_rule_regex)}.sort.index(my_rule) + 1 + unnamed_offset + end +end diff --git a/firewall/lib/puppet/provider/firewallchain/iptables_chain.rb b/firewall/lib/puppet/provider/firewallchain/iptables_chain.rb new file mode 100644 index 000000000..df166f645 --- /dev/null +++ b/firewall/lib/puppet/provider/firewallchain/iptables_chain.rb @@ -0,0 +1,179 @@ +Puppet::Type.type(:firewallchain).provide :iptables_chain do + include Puppet::Util::Firewall + + @doc = "Iptables chain provider" + + has_feature :iptables_chain + has_feature :policy + + optional_commands({ + :iptables => 'iptables', + :iptables_save => 'iptables-save', + :ip6tables => 'ip6tables', + :ip6tables_save => 'ip6tables-save', + :ebtables => 'ebtables', + :ebtables_save => 'ebtables-save', + }) + + defaultfor :kernel => :linux + confine :kernel => :linux + + # chain name is greedy so we anchor from the end. + # [\d+:\d+] doesn't exist on ebtables + Mapping = { + :IPv4 => { + :tables => method(:iptables), + :save => method(:iptables_save), + :re => /^:(.+)\s(\S+)\s\[\d+:\d+\]$/, + }, + :IPv6 => { + :tables => method(:ip6tables), + :save => method(:ip6tables_save), + :re => /^:(.+)\s(\S+)\s\[\d+:\d+\]$/, + }, + :ethernet => { + :tables => method(:ebtables), + :save => method(:ebtables_save), + :re => /^:(.+)\s(\S+)$/, + } + } + InternalChains = /^(PREROUTING|POSTROUTING|BROUTING|INPUT|FORWARD|OUTPUT)$/ + Tables = 'nat|mangle|filter|raw|rawpost|broute' + Nameformat = /^(.+):(#{Tables}):(IP(v[46])?|ethernet)$/ + + def create + allvalidchains do |t, chain, table, protocol| + if chain =~ InternalChains + # can't create internal chains + warning "Attempting to create internal chain #{@resource[:name]}" + end + if properties[:ensure] == protocol + debug "Skipping Inserting chain #{chain} on table #{table} (#{protocol}) already exists" + else + debug "Inserting chain #{chain} on table #{table} (#{protocol}) using #{t}" + t.call ['-t',table,'-N',chain] + unless @resource[:policy].nil? + t.call ['-t',table,'-P',chain,@resource[:policy].to_s.upcase] + end + end + end + end + + def destroy + allvalidchains do |t, chain, table| + if chain =~ InternalChains + # can't delete internal chains + warning "Attempting to destroy internal chain #{@resource[:name]}" + end + debug "Deleting chain #{chain} on table #{table}" + t.call ['-t',table,'-X',chain] + end + end + + def exists? + allvalidchains do |t, chain| + if chain =~ InternalChains + # If the chain isn't present, it's likely because the module isn't loaded. + # If this is true, then we fall into 2 cases + # 1) It'll be loaded on demand + # 2) It won't be loaded on demand, and we throw an error + # This is the intended behavior as it's not the provider's job to load kernel modules + # So we pretend it exists... + return true + end + end + properties[:ensure] == :present + end + + def policy=(value) + return if value == :empty + allvalidchains do |t, chain, table| + p = ['-t',table,'-P',chain,value.to_s.upcase] + debug "[set policy] #{t} #{p}" + t.call p + end + end + + def policy + debug "[get policy] #{@resource[:name]} =#{@property_hash[:policy].to_s.downcase}" + return @property_hash[:policy].to_s.downcase + end + + def self.prefetch(resources) + debug("[prefetch(resources)]") + instances.each do |prov| + if resource = resources[prov.name] + resource.provider = prov + end + end + end + + def flush + debug("[flush]") + persist_iptables(@resource[:name].match(Nameformat)[3]) + # Clear the property hash so we re-initialize with updated values + @property_hash.clear + end + + # Look up the current status. This allows us to conventiently look up + # existing status with properties[:foo]. + def properties + if @property_hash.empty? + @property_hash = query || {:ensure => :absent} + end + @property_hash.dup + end + + # Pull the current state of the list from the full list. + def query + self.class.instances.each do |instance| + if instance.name == self.name + debug "query found #{self.name}" % instance.properties.inspect + return instance.properties + end + end + nil + end + + def self.instances + debug "[instances]" + table = nil + chains = [] + + Mapping.each { |p, c| + begin + c[:save].call.each_line do |line| + if line =~ c[:re] then + name = $1 + ':' + (table == 'filter' ? 'filter' : table) + ':' + p.to_s + policy = $2 == '-' ? nil : $2.downcase.to_sym + + chains << new({ + :name => name, + :policy => policy, + :ensure => :present, + }) + + debug "[instance] '#{name}' #{policy}" + elsif line =~ /^\*(\S+)/ + table = $1 + else + next + end + end + rescue Puppet::Error + # ignore command not found for ebtables or anything that doesn't exist + end + } + + chains + end + + def allvalidchains + @resource[:name].match(Nameformat) + chain = $1 + table = $2 + protocol = $3 + yield Mapping[protocol.to_sym][:tables],chain,table,protocol.to_sym + end + +end diff --git a/firewall/lib/puppet/type/firewall.rb b/firewall/lib/puppet/type/firewall.rb new file mode 100644 index 000000000..34a5d33a6 --- /dev/null +++ b/firewall/lib/puppet/type/firewall.rb @@ -0,0 +1,1147 @@ +# See: #10295 for more details. +# +# This is a workaround for bug: #4248 whereby ruby files outside of the normal +# provider/type path do not load until pluginsync has occured on the puppetmaster +# +# In this case I'm trying the relative path first, then falling back to normal +# mechanisms. This should be fixed in future versions of puppet but it looks +# like we'll need to maintain this for some time perhaps. +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__),"..","..")) +require 'puppet/util/firewall' + +Puppet::Type.newtype(:firewall) do + include Puppet::Util::Firewall + + @doc = <<-EOS + This type provides the capability to manage firewall rules within + puppet. + + **Autorequires:** + + If Puppet is managing the iptables or ip6tables chains specified in the + `chain` or `jump` parameters, the firewall resource will autorequire + those firewallchain resources. + + If Puppet is managing the iptables, iptables-persistent, or iptables-services packages, + and the provider is iptables or ip6tables, the firewall resource will + autorequire those packages to ensure that any required binaries are + installed. + EOS + + feature :connection_limiting, "Connection limiting features." + feature :hop_limiting, "Hop limiting features." + feature :rate_limiting, "Rate limiting features." + feature :recent_limiting, "The netfilter recent module" + feature :snat, "Source NATing" + feature :dnat, "Destination NATing" + feature :interface_match, "Interface matching" + feature :icmp_match, "Matching ICMP types" + feature :owner, "Matching owners" + feature :state_match, "Matching stateful firewall states" + feature :reject_type, "The ability to control reject messages" + feature :log_level, "The ability to control the log level" + feature :log_prefix, "The ability to add prefixes to log messages" + feature :mark, "Match or Set the netfilter mark value associated with the packet" + feature :tcp_flags, "The ability to match on particular TCP flag settings" + feature :pkttype, "Match a packet type" + feature :socket, "Match open sockets" + feature :isfragment, "Match fragments" + feature :address_type, "The ability match on source or destination address type" + feature :iprange, "The ability match on source or destination IP range " + feature :ishasmorefrags, "Match a non-last fragment of a fragmented ipv6 packet - might be first" + feature :islastfrag, "Match the last fragment of an ipv6 packet" + feature :isfirstfrag, "Match the first fragment of a fragmented ipv6 packet" + feature :ipsec_policy, "Match IPsec policy" + feature :ipsec_dir, "Match IPsec policy direction" + feature :mask, "Ability to match recent rules based on the ipv4 mask" + + # provider specific features + feature :iptables, "The provider provides iptables features." + + ensurable do + desc <<-EOS + Manage the state of this rule. The default action is *present*. + EOS + + newvalue(:present) do + provider.insert + end + + newvalue(:absent) do + provider.delete + end + + defaultto :present + end + + newparam(:name) do + desc <<-EOS + The canonical name of the rule. This name is also used for ordering + so make sure you prefix the rule with a number: + + 000 this runs first + 999 this runs last + + Depending on the provider, the name of the rule can be stored using + the comment feature of the underlying firewall subsystem. + EOS + isnamevar + + # Keep rule names simple - they must start with a number + newvalues(/^\d+[[:alpha:][:digit:][:punct:][:space:]]+$/) + end + + newproperty(:action) do + desc <<-EOS + This is the action to perform on a match. Can be one of: + + * accept - the packet is accepted + * reject - the packet is rejected with a suitable ICMP response + * drop - the packet is dropped + + If you specify no value it will simply match the rule but perform no + action unless you provide a provider specific parameter (such as *jump*). + EOS + newvalues(:accept, :reject, :drop) + end + + # Generic matching properties + newproperty(:source) do + desc <<-EOS + The source address. For example: + + source => '192.168.2.0/24' + + You can also negate a mask by putting ! in front. For example: + + source => '! 192.168.2.0/24' + + The source can also be an IPv6 address if your provider supports it. + EOS + + munge do |value| + begin + @resource.host_to_mask(value) + rescue Exception => e + self.fail("host_to_ip failed for #{value}, exception #{e}") + end + end + end + + # Source IP range + newproperty(:src_range, :required_features => :iprange) do + desc <<-EOS + The source IP range. For example: + + src_range => '192.168.1.1-192.168.1.10' + + The source IP range is must in 'IP1-IP2' format. + EOS + + newvalues(/^((25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)-((25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)/) + end + + newproperty(:destination) do + desc <<-EOS + The destination address to match. For example: + + destination => '192.168.1.0/24' + + You can also negate a mask by putting ! in front. For example: + + destination => '! 192.168.2.0/24' + + The destination can also be an IPv6 address if your provider supports it. + EOS + + munge do |value| + begin + @resource.host_to_mask(value) + rescue Exception => e + self.fail("host_to_ip failed for #{value}, exception #{e}") + end + end + end + + # Destination IP range + newproperty(:dst_range, :required_features => :iprange) do + desc <<-EOS + The destination IP range. For example: + + dst_range => '192.168.1.1-192.168.1.10' + + The destination IP range is must in 'IP1-IP2' format. + EOS + + newvalues(/^((25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)-((25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)/) + end + + newproperty(:sport, :array_matching => :all) do + desc <<-EOS + The source port to match for this filter (if the protocol supports + ports). Will accept a single element or an array. + + For some firewall providers you can pass a range of ports in the format: + + - + + For example: + + 1-1024 + + This would cover ports 1 to 1024. + EOS + + munge do |value| + @resource.string_to_port(value, :proto) + end + + def is_to_s(value) + should_to_s(value) + end + + def should_to_s(value) + value = [value] unless value.is_a?(Array) + value.join(',') + end + end + + newproperty(:dport, :array_matching => :all) do + desc <<-EOS + The destination port to match for this filter (if the protocol supports + ports). Will accept a single element or an array. + + For some firewall providers you can pass a range of ports in the format: + + - + + For example: + + 1-1024 + + This would cover ports 1 to 1024. + EOS + + munge do |value| + @resource.string_to_port(value, :proto) + end + + def is_to_s(value) + should_to_s(value) + end + + def should_to_s(value) + value = [value] unless value.is_a?(Array) + value.join(',') + end + end + + newproperty(:port, :array_matching => :all) do + desc <<-EOS + The destination or source port to match for this filter (if the protocol + supports ports). Will accept a single element or an array. + + For some firewall providers you can pass a range of ports in the format: + + - + + For example: + + 1-1024 + + This would cover ports 1 to 1024. + EOS + + munge do |value| + @resource.string_to_port(value, :proto) + end + + def is_to_s(value) + should_to_s(value) + end + + def should_to_s(value) + value = [value] unless value.is_a?(Array) + value.join(',') + end + end + + newproperty(:dst_type, :required_features => :address_type) do + desc <<-EOS + The destination address type. For example: + + dst_type => 'LOCAL' + + Can be one of: + + * UNSPEC - an unspecified address + * UNICAST - a unicast address + * LOCAL - a local address + * BROADCAST - a broadcast address + * ANYCAST - an anycast packet + * MULTICAST - a multicast address + * BLACKHOLE - a blackhole address + * UNREACHABLE - an unreachable address + * PROHIBIT - a prohibited address + * THROW - undocumented + * NAT - undocumented + * XRESOLVE - undocumented + EOS + + newvalues(:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, + :BLACKHOLE, :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE) + end + + newproperty(:src_type, :required_features => :address_type) do + desc <<-EOS + The source address type. For example: + + src_type => 'LOCAL' + + Can be one of: + + * UNSPEC - an unspecified address + * UNICAST - a unicast address + * LOCAL - a local address + * BROADCAST - a broadcast address + * ANYCAST - an anycast packet + * MULTICAST - a multicast address + * BLACKHOLE - a blackhole address + * UNREACHABLE - an unreachable address + * PROHIBIT - a prohibited address + * THROW - undocumented + * NAT - undocumented + * XRESOLVE - undocumented + EOS + + newvalues(:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, + :BLACKHOLE, :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE) + end + + newproperty(:proto) do + desc <<-EOS + The specific protocol to match for this rule. By default this is + *tcp*. + EOS + + newvalues(*[:tcp, :udp, :icmp, :"ipv6-icmp", :esp, :ah, :vrrp, :igmp, :ipencap, :ospf, :gre, :cbt, :all].collect do |proto| + [proto, "! #{proto}".to_sym] + end.flatten) + defaultto "tcp" + end + + # tcp-specific + newproperty(:tcp_flags, :required_features => :tcp_flags) do + desc <<-EOS + Match when the TCP flags are as specified. + Is a string with a list of comma-separated flag names for the mask, + then a space, then a comma-separated list of flags that should be set. + The flags are: SYN ACK FIN RST URG PSH ALL NONE + Note that you specify them in the order that iptables --list-rules + would list them to avoid having puppet think you changed the flags. + Example: FIN,SYN,RST,ACK SYN matches packets with the SYN bit set and the + ACK,RST and FIN bits cleared. Such packets are used to request + TCP connection initiation. + EOS + end + + + # Iptables specific + newproperty(:chain, :required_features => :iptables) do + desc <<-EOS + Name of the chain to use. Can be one of the built-ins: + + * INPUT + * FORWARD + * OUTPUT + * PREROUTING + * POSTROUTING + + Or you can provide a user-based chain. + + The default value is 'INPUT'. + EOS + + defaultto "INPUT" + newvalue(/^[a-zA-Z0-9\-_]+$/) + end + + newproperty(:table, :required_features => :iptables) do + desc <<-EOS + Table to use. Can be one of: + + * nat + * mangle + * filter + * raw + * rawpost + + By default the setting is 'filter'. + EOS + + newvalues(:nat, :mangle, :filter, :raw, :rawpost) + defaultto "filter" + end + + newproperty(:jump, :required_features => :iptables) do + desc <<-EOS + The value for the iptables --jump parameter. Normal values are: + + * QUEUE + * RETURN + * DNAT + * SNAT + * LOG + * MASQUERADE + * REDIRECT + * MARK + + But any valid chain name is allowed. + + For the values ACCEPT, DROP and REJECT you must use the generic + 'action' parameter. This is to enfore the use of generic parameters where + possible for maximum cross-platform modelling. + + If you set both 'accept' and 'jump' parameters, you will get an error as + only one of the options should be set. + EOS + + validate do |value| + unless value =~ /^[a-zA-Z0-9\-_]+$/ + raise ArgumentError, <<-EOS + Jump destination must consist of alphanumeric characters, an + underscore or a yphen. + EOS + end + + if ["accept","reject","drop"].include?(value.downcase) + raise ArgumentError, <<-EOS + Jump destination should not be one of ACCEPT, REJECT or DROP. Use + the action property instead. + EOS + end + + end + end + + # Interface specific matching properties + newproperty(:iniface, :required_features => :interface_match) do + desc <<-EOS + Input interface to filter on. + EOS + newvalues(/^[a-zA-Z0-9\-\._\+]+$/) + end + + newproperty(:outiface, :required_features => :interface_match) do + desc <<-EOS + Output interface to filter on. + EOS + newvalues(/^[a-zA-Z0-9\-\._\+]+$/) + end + + # NAT specific properties + newproperty(:tosource, :required_features => :snat) do + desc <<-EOS + When using jump => "SNAT" you can specify the new source address using + this parameter. + EOS + end + + newproperty(:todest, :required_features => :dnat) do + desc <<-EOS + When using jump => "DNAT" you can specify the new destination address + using this paramter. + EOS + end + + newproperty(:toports, :required_features => :dnat) do + desc <<-EOS + For DNAT this is the port that will replace the destination port. + EOS + end + + newproperty(:random, :required_features => :dnat) do + desc <<-EOS + When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" + this boolean will enable randomized port mapping. + EOS + + newvalues(:true, :false) + end + + # Reject ICMP type + newproperty(:reject, :required_features => :reject_type) do + desc <<-EOS + When combined with jump => "REJECT" you can specify a different icmp + response to be sent back to the packet sender. + EOS + end + + # Logging properties + newproperty(:log_level, :required_features => :log_level) do + desc <<-EOS + When combined with jump => "LOG" specifies the system log level to log + to. + EOS + + munge do |value| + if value.kind_of?(String) + value = @resource.log_level_name_to_number(value) + else + value + end + + if value == nil && value != "" + self.fail("Unable to determine log level") + end + value + end + end + + newproperty(:log_prefix, :required_features => :log_prefix) do + desc <<-EOS + When combined with jump => "LOG" specifies the log prefix to use when + logging. + EOS + end + + # ICMP matching property + newproperty(:icmp, :required_features => :icmp_match) do + desc <<-EOS + When matching ICMP packets, this is the type of ICMP packet to match. + + A value of "any" is not supported. To achieve this behaviour the + parameter should simply be omitted or undefined. + EOS + + validate do |value| + if value == "any" + raise ArgumentError, + "Value 'any' is not valid. This behaviour should be achieved " \ + "by omitting or undefining the ICMP parameter." + end + end + + munge do |value| + if value.kind_of?(String) + # ICMP codes differ between IPv4 and IPv6. + case @resource[:provider] + when :iptables + protocol = 'inet' + when :ip6tables + protocol = 'inet6' + else + self.fail("cannot work out protocol family") + end + + value = @resource.icmp_name_to_number(value, protocol) + else + value + end + + if value == nil && value != "" + self.fail("cannot work out icmp type") + end + value + end + end + + newproperty(:state, :array_matching => :all, :required_features => + :state_match) do + + desc <<-EOS + Matches a packet based on its state in the firewall stateful inspection + table. Values can be: + + * INVALID + * ESTABLISHED + * NEW + * RELATED + EOS + + newvalues(:INVALID,:ESTABLISHED,:NEW,:RELATED) + + # States should always be sorted. This normalizes the resource states to + # keep it consistent with the sorted result from iptables-save. + def should=(values) + @should = super(values).sort_by {|sym| sym.to_s} + end + + def is_to_s(value) + should_to_s(value) + end + + def should_to_s(value) + value = [value] unless value.is_a?(Array) + value.join(',') + end + end + + newproperty(:ctstate, :array_matching => :all, :required_features => + :state_match) do + + desc <<-EOS + Matches a packet based on its state in the firewall stateful inspection + table, using the conntrack module. Values can be: + + * INVALID + * ESTABLISHED + * NEW + * RELATED + EOS + + newvalues(:INVALID,:ESTABLISHED,:NEW,:RELATED) + + # States should always be sorted. This normalizes the resource states to + # keep it consistent with the sorted result from iptables-save. + def should=(values) + @should = super(values).sort_by {|sym| sym.to_s} + end + + def is_to_s(value) + should_to_s(value) + end + + def should_to_s(value) + value = [value] unless value.is_a?(Array) + value.join(',') + end + end + + + # Connection mark + newproperty(:connmark, :required_features => :mark) do + desc <<-EOS + Match the Netfilter mark value associated with the packet. Accepts either of: + mark/mask or mark. These will be converted to hex if they are not already. + EOS + munge do |value| + int_or_hex = '[a-fA-F0-9x]' + match = value.to_s.match("(#{int_or_hex}+)(/)?(#{int_or_hex}+)?") + mark = @resource.to_hex32(match[1]) + + # Values that can't be converted to hex. + # Or contain a trailing slash with no mask. + if mark.nil? or (mark and match[2] and match[3].nil?) + raise ArgumentError, "MARK value must be integer or hex between 0 and 0xffffffff" + end + + # There should not be a mask on connmark + unless match[3].nil? + raise ArgumentError, "iptables does not support masks on MARK match rules" + end + value = mark + + value + end + end + + # Connection limiting properties + newproperty(:connlimit_above, :required_features => :connection_limiting) do + desc <<-EOS + Connection limiting value for matched connections above n. + EOS + newvalue(/^\d+$/) + end + + newproperty(:connlimit_mask, :required_features => :connection_limiting) do + desc <<-EOS + Connection limiting by subnet mask for matched connections. + IPv4: 0-32 + IPv6: 0-128 + EOS + newvalue(/^\d+$/) + end + + # Hop limiting properties + newproperty(:hop_limit, :required_features => :hop_limiting) do + desc <<-EOS + Hop limiting value for matched packets. + EOS + newvalue(/^\d+$/) + end + + # Rate limiting properties + newproperty(:limit, :required_features => :rate_limiting) do + desc <<-EOS + Rate limiting value for matched packets. The format is: + rate/[/second/|/minute|/hour|/day]. + + Example values are: '50/sec', '40/min', '30/hour', '10/day'." + EOS + end + + newproperty(:burst, :required_features => :rate_limiting) do + desc <<-EOS + Rate limiting burst value (per second) before limit checks apply. + EOS + newvalue(/^\d+$/) + end + + newproperty(:uid, :required_features => :owner) do + desc <<-EOS + UID or Username owner matching rule. Accepts a string argument + only, as iptables does not accept multiple uid in a single + statement. + EOS + end + + newproperty(:gid, :required_features => :owner) do + desc <<-EOS + GID or Group owner matching rule. Accepts a string argument + only, as iptables does not accept multiple gid in a single + statement. + EOS + end + + newproperty(:set_mark, :required_features => :mark) do + desc <<-EOS + Set the Netfilter mark value associated with the packet. Accepts either of: + mark/mask or mark. These will be converted to hex if they are not already. + EOS + + munge do |value| + int_or_hex = '[a-fA-F0-9x]' + match = value.to_s.match("(#{int_or_hex}+)(/)?(#{int_or_hex}+)?") + mark = @resource.to_hex32(match[1]) + + # Values that can't be converted to hex. + # Or contain a trailing slash with no mask. + if mark.nil? or (mark and match[2] and match[3].nil?) + raise ArgumentError, "MARK value must be integer or hex between 0 and 0xffffffff" + end + + # Old iptables does not support a mask. New iptables will expect one. + iptables_version = Facter.fact('iptables_version').value + mask_required = (iptables_version and Puppet::Util::Package.versioncmp(iptables_version, '1.4.1') >= 0) + + if mask_required + if match[3].nil? + value = "#{mark}/0xffffffff" + else + mask = @resource.to_hex32(match[3]) + if mask.nil? + raise ArgumentError, "MARK mask must be integer or hex between 0 and 0xffffffff" + end + value = "#{mark}/#{mask}" + end + else + unless match[3].nil? + raise ArgumentError, "iptables version #{iptables_version} does not support masks on MARK rules" + end + value = mark + end + + value + end + end + + newproperty(:pkttype, :required_features => :pkttype) do + desc <<-EOS + Sets the packet type to match. + EOS + + newvalues(:unicast, :broadcast, :multicast) + end + + newproperty(:isfragment, :required_features => :isfragment) do + desc <<-EOS + Set to true to match tcp fragments (requires type to be set to tcp) + EOS + + newvalues(:true, :false) + end + + newproperty(:recent, :required_features => :recent_limiting) do + desc <<-EOS + Enable the recent module. Takes as an argument one of set, update, + rcheck or remove. For example: + + # If anyone's appeared on the 'badguy' blacklist within + # the last 60 seconds, drop their traffic, and update the timestamp. + firewall { '100 Drop badguy traffic': + recent => 'update', + rseconds => 60, + rsource => true, + rname => 'badguy', + action => 'DROP', + chain => 'FORWARD', + } + # No-one should be sending us traffic on eth0 from localhost + # Blacklist them + firewall { '101 blacklist strange traffic': + recent => 'set', + rsource => true, + rname => 'badguy', + destination => '127.0.0.0/8', + iniface => 'eth0', + action => 'DROP', + chain => 'FORWARD', + } + EOS + + newvalues(:set, :update, :rcheck, :remove) + munge do |value| + value = "--" + value + end + end + + newproperty(:rdest, :required_features => :recent_limiting) do + desc <<-EOS + Recent module; add the destination IP address to the list. + Must be boolean true. + EOS + + newvalues(:true, :false) + end + + newproperty(:rsource, :required_features => :recent_limiting) do + desc <<-EOS + Recent module; add the source IP address to the list. + Must be boolean true. + EOS + + newvalues(:true, :false) + end + + newproperty(:rname, :required_features => :recent_limiting) do + desc <<-EOS + Recent module; The name of the list. Takes a string argument. + EOS + end + + newproperty(:rseconds, :required_features => :recent_limiting) do + desc <<-EOS + Recent module; used in conjunction with one of `recent => 'rcheck'` or + `recent => 'update'`. When used, this will narrow the match to only + happen when the address is in the list and was seen within the last given + number of seconds. + EOS + end + + newproperty(:reap, :required_features => :recent_limiting) do + desc <<-EOS + Recent module; can only be used in conjunction with the `rseconds` + attribute. When used, this will cause entries older than 'seconds' to be + purged. Must be boolean true. + EOS + + newvalues(:true, :false) + end + + newproperty(:rhitcount, :required_features => :recent_limiting) do + desc <<-EOS + Recent module; used in conjunction with `recent => 'update'` or `recent + => 'rcheck'. When used, this will narrow the match to only happen when + the address is in the list and packets had been received greater than or + equal to the given value. + EOS + end + + newproperty(:rttl, :required_features => :recent_limiting) do + desc <<-EOS + Recent module; may only be used in conjunction with one of `recent => + 'rcheck'` or `recent => 'update'`. When used, this will narrow the match + to only happen when the address is in the list and the TTL of the current + packet matches that of the packet which hit the `recent => 'set'` rule. + This may be useful if you have problems with people faking their source + address in order to DoS you via this module by disallowing others access + to your site by sending bogus packets to you. Must be boolean true. + EOS + + newvalues(:true, :false) + end + + newproperty(:socket, :required_features => :socket) do + desc <<-EOS + If true, matches if an open socket can be found by doing a coket lookup + on the packet. + EOS + + newvalues(:true, :false) + end + + newproperty(:ishasmorefrags, :required_features => :ishasmorefrags) do + desc <<-EOS + If true, matches if the packet has it's 'more fragments' bit set. ipv6. + EOS + + newvalues(:true, :false) + end + + newproperty(:islastfrag, :required_features => :islastfrag) do + desc <<-EOS + If true, matches if the packet is the last fragment. ipv6. + EOS + + newvalues(:true, :false) + end + + newproperty(:isfirstfrag, :required_features => :isfirstfrag) do + desc <<-EOS + If true, matches if the packet is the first fragment. + Sadly cannot be negated. ipv6. + EOS + + newvalues(:true, :false) + end + + newproperty(:ipsec_policy, :required_features => :ipsec_policy) do + desc <<-EOS + Sets the ipsec policy type. May take a combination of arguments for any flags that can be passed to `--pol ipsec` such as: `--strict`, `--reqid 100`, `--next`, `--proto esp`, etc. + EOS + + newvalues(:none, :ipsec) + end + + newproperty(:ipsec_dir, :required_features => :ipsec_dir) do + desc <<-EOS + Sets the ipsec policy direction + EOS + + newvalues(:in, :out) + end + + newproperty(:stat_mode) do + desc <<-EOS + Set the matching mode for statistic matching. Supported modes are `random` and `nth`. + EOS + + newvalues(:nth, :random) + end + + newproperty(:stat_every) do + desc <<-EOS + Match one packet every nth packet. Requires `stat_mode => 'nth'` + EOS + + validate do |value| + unless value =~ /^\d+$/ + raise ArgumentError, <<-EOS + stat_every value must be a digit + EOS + end + + unless value.to_i > 0 + raise ArgumentError, <<-EOS + stat_every value must be larger than 0 + EOS + end + end + end + + newproperty(:stat_packet) do + desc <<-EOS + Set the initial counter value for the nth mode. Must be between 0 and the value of `stat_every`. Defaults to 0. Requires `stat_mode => 'nth'` + EOS + + newvalues(/^\d+$/) + end + + newproperty(:stat_probability) do + desc <<-EOS + Set the probability from 0 to 1 for a packet to be randomly matched. It works only with `stat_mode => 'random'`. + EOS + + validate do |value| + unless value =~ /^([01])\.(\d+)$/ + raise ArgumentError, <<-EOS + stat_probability must be between 0.0 and 1.0 + EOS + end + + if $1.to_i == 1 && $2.to_i != 0 + raise ArgumentError, <<-EOS + start_probability must be between 0.0 and 1.0 + EOS + end + end + end + + newproperty(:mask, :required_features => :mask) do + desc <<-EOS + Sets the mask to use when `recent` is enabled. + EOS + end + + newparam(:line) do + desc <<-EOS + Read-only property for caching the rule line. + EOS + end + + newproperty(:mac_source) do + desc <<-EOS + MAC Source + EOS + newvalues(/^([0-9a-f]{2}[:]){5}([0-9a-f]{2})$/i) + end + + autorequire(:firewallchain) do + reqs = [] + protocol = nil + + case value(:provider) + when :iptables + protocol = "IPv4" + when :ip6tables + protocol = "IPv6" + end + + unless protocol.nil? + table = value(:table) + [value(:chain), value(:jump)].each do |chain| + reqs << "#{chain}:#{table}:#{protocol}" unless ( chain.nil? || (['INPUT', 'OUTPUT', 'FORWARD'].include?(chain) && table == :filter) ) + end + end + + reqs + end + + # Classes would be a better abstraction, pending: + # http://projects.puppetlabs.com/issues/19001 + autorequire(:package) do + case value(:provider) + when :iptables, :ip6tables + %w{iptables iptables-persistent iptables-services} + else + [] + end + end + + validate do + debug("[validate]") + + # TODO: this is put here to skip validation if ensure is not set. This + # is because there is a revalidation stage called later where the values + # are not set correctly. I tried tracing it - but have put in this + # workaround instead to skip. Must get to the bottom of this. + if ! value(:ensure) + return + end + + # First we make sure the chains and tables are valid combinations + if value(:table).to_s == "filter" && + value(:chain) =~ /PREROUTING|POSTROUTING/ + + self.fail "PREROUTING and POSTROUTING cannot be used in table 'filter'" + end + + if value(:table).to_s == "nat" && value(:chain) =~ /INPUT|FORWARD/ + self.fail "INPUT and FORWARD cannot be used in table 'nat'" + end + + if value(:table).to_s == "raw" && + value(:chain) =~ /INPUT|FORWARD|POSTROUTING/ + + self.fail "INPUT, FORWARD and POSTROUTING cannot be used in table raw" + end + + # Now we analyse the individual properties to make sure they apply to + # the correct combinations. + if value(:uid) + unless value(:chain).to_s =~ /OUTPUT|POSTROUTING/ + self.fail "Parameter uid only applies to chains " \ + "OUTPUT,POSTROUTING" + end + end + + if value(:gid) + unless value(:chain).to_s =~ /OUTPUT|POSTROUTING/ + self.fail "Parameter gid only applies to chains " \ + "OUTPUT,POSTROUTING" + end + end + + if value(:set_mark) + unless value(:jump).to_s =~ /MARK/ && + value(:chain).to_s =~ /PREROUTING|OUTPUT/ && + value(:table).to_s =~ /mangle/ + self.fail "Parameter set_mark only applies to " \ + "the PREROUTING or OUTPUT chain of the mangle table and when jump => MARK" + end + end + + if value(:dport) + unless value(:proto).to_s =~ /tcp|udp|sctp/ + self.fail "[%s] Parameter dport only applies to sctp, tcp and udp " \ + "protocols. Current protocol is [%s] and dport is [%s]" % + [value(:name), should(:proto), should(:dport)] + end + end + + if value(:jump).to_s == "DNAT" + unless value(:table).to_s =~ /nat/ + self.fail "Parameter jump => DNAT only applies to table => nat" + end + + unless value(:todest) + self.fail "Parameter jump => DNAT must have todest parameter" + end + end + + if value(:jump).to_s == "SNAT" + unless value(:table).to_s =~ /nat/ + self.fail "Parameter jump => SNAT only applies to table => nat" + end + + unless value(:tosource) + self.fail "Parameter jump => SNAT must have tosource parameter" + end + end + + if value(:jump).to_s == "REDIRECT" + unless value(:toports) + self.fail "Parameter jump => REDIRECT missing mandatory toports " \ + "parameter" + end + end + + if value(:jump).to_s == "MASQUERADE" + unless value(:table).to_s =~ /nat/ + self.fail "Parameter jump => MASQUERADE only applies to table => nat" + end + end + + if value(:log_prefix) || value(:log_level) + unless value(:jump).to_s == "LOG" + self.fail "Parameter log_prefix and log_level require jump => LOG" + end + end + + if value(:burst) && ! value(:limit) + self.fail "burst makes no sense without limit" + end + + if value(:action) && value(:jump) + self.fail "Only one of the parameters 'action' and 'jump' can be set" + end + + if value(:connlimit_mask) && ! value(:connlimit_above) + self.fail "Parameter 'connlimit_mask' requires 'connlimit_above'" + end + + if value(:mask) && ! value(:recent) + self.fail "Mask can only be set if recent is enabled." + end + + [:stat_packet, :stat_every, :stat_probability].each do |param| + if value(param) && ! value(:stat_mode) + self.fail "Parameter '#{param.to_s}' requires 'stat_mode' to be set" + end + end + + if value(:stat_packet) && value(:stat_mode) != :nth + self.fail "Parameter 'stat_packet' requires 'stat_mode' to be set to 'nth'" + end + + if value(:stat_every) && value(:stat_mode) != :nth + self.fail "Parameter 'stat_every' requires 'stat_mode' to be set to 'nth'" + end + + if value(:stat_probability) && value(:stat_mode) != :random + self.fail "Parameter 'stat_probability' requires 'stat_mode' to be set to 'random'" + end + + end +end diff --git a/firewall/lib/puppet/type/firewallchain.rb b/firewall/lib/puppet/type/firewallchain.rb new file mode 100644 index 000000000..b962a0a36 --- /dev/null +++ b/firewall/lib/puppet/type/firewallchain.rb @@ -0,0 +1,222 @@ +# This is a workaround for bug: #4248 whereby ruby files outside of the normal +# provider/type path do not load until pluginsync has occured on the puppetmaster +# +# In this case I'm trying the relative path first, then falling back to normal +# mechanisms. This should be fixed in future versions of puppet but it looks +# like we'll need to maintain this for some time perhaps. +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__),"..","..")) +require 'puppet/util/firewall' + +Puppet::Type.newtype(:firewallchain) do + include Puppet::Util::Firewall + + @doc = <<-EOS + This type provides the capability to manage rule chains for firewalls. + + Currently this supports only iptables, ip6tables and ebtables on Linux. And + provides support for setting the default policy on chains and tables that + allow it. + + **Autorequires:** + If Puppet is managing the iptables, iptables-persistent, or iptables-services packages, + and the provider is iptables_chain, the firewall resource will autorequire + those packages to ensure that any required binaries are installed. + EOS + + feature :iptables_chain, "The provider provides iptables chain features." + feature :policy, "Default policy (inbuilt chains only)" + + ensurable do + defaultvalues + defaultto :present + end + + newparam(:name) do + desc <<-EOS + The canonical name of the chain. + + For iptables the format must be {chain}:{table}:{protocol}. + EOS + isnamevar + + validate do |value| + if value !~ Nameformat then + raise ArgumentError, "Inbuilt chains must be in the form {chain}:{table}:{protocol} where {table} is one of FILTER, NAT, MANGLE, RAW, RAWPOST, BROUTE or empty (alias for filter), chain can be anything without colons or one of PREROUTING, POSTROUTING, BROUTING, INPUT, FORWARD, OUTPUT for the inbuilt chains, and {protocol} being IPv4, IPv6, ethernet (ethernet bridging) got '#{value}' table:'#{$1}' chain:'#{$2}' protocol:'#{$3}'" + else + chain = $1 + table = $2 + protocol = $3 + case table + when 'filter' + if chain =~ /^(PREROUTING|POSTROUTING|BROUTING)$/ + raise ArgumentError, "INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table 'filter'" + end + when 'mangle' + if chain =~ InternalChains && chain == 'BROUTING' + raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table 'mangle'" + end + when 'nat' + if chain =~ /^(BROUTING|FORWARD)$/ + raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, and OUTPUT are the only inbuilt chains that can be used in table 'nat'" + end + if protocol =~/^(IP(v6)?)?$/ + raise ArgumentError, "table nat isn't valid in IPv6. You must specify ':IPv4' as the name suffix" + end + when 'raw' + if chain =~ /^(POSTROUTING|BROUTING|INPUT|FORWARD)$/ + raise ArgumentError,'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\'' + end + when 'broute' + if protocol != 'ethernet' + raise ArgumentError,'BROUTE is only valid with protocol \'ethernet\'' + end + if chain =~ /^PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT$/ + raise ArgumentError,'BROUTING is the only inbuilt chain allowed on on table \'broute\'' + end + end + if chain == 'BROUTING' && ( protocol != 'ethernet' || table!='broute') + raise ArgumentError,'BROUTING is the only inbuilt chain allowed on on table \'BROUTE\' with protocol \'ethernet\' i.e. \'broute:BROUTING:enternet\'' + end + end + end + end + + newproperty(:policy) do + desc <<-EOS + This is the action to when the end of the chain is reached. + It can only be set on inbuilt chains (INPUT, FORWARD, OUTPUT, + PREROUTING, POSTROUTING) and can be one of: + + * accept - the packet is accepted + * drop - the packet is dropped + * queue - the packet is passed userspace + * return - the packet is returned to calling (jump) queue + or the default of inbuilt chains + EOS + newvalues(:accept, :drop, :queue, :return) + defaultto do + # ethernet chain have an ACCEPT default while other haven't got an + # allowed value + if @resource[:name] =~ /:ethernet$/ + :accept + else + nil + end + end + end + + newparam(:purge, :boolean => true) do + desc <<-EOS + Purge unmanaged firewall rules in this chain + EOS + newvalues(:false, :true) + defaultto :false + end + + newparam(:ignore) do + desc <<-EOS + Regex to perform on firewall rules to exempt unmanaged rules from purging (when enabled). + This is matched against the output of `iptables-save`. + + This can be a single regex, or an array of them. + To support flags, use the ruby inline flag mechanism. + Meaning a regex such as + /foo/i + can be written as + '(?i)foo' or '(?i:foo)' + + Full example: + firewallchain { 'INPUT:filter:IPv4': + purge => true, + ignore => [ + '-j fail2ban-ssh', # ignore the fail2ban jump rule + '--comment "[^"]*(?i:ignore)[^"]*"', # ignore any rules with "ignore" (case insensitive) in the comment in the rule + ], + } + EOS + + validate do |value| + unless value.is_a?(Array) or value.is_a?(String) or value == false + self.devfail "Ignore must be a string or an Array" + end + end + munge do |patterns| # convert into an array of {Regex}es + patterns = [patterns] if patterns.is_a?(String) + patterns.map{|p| Regexp.new(p)} + end + end + + # Classes would be a better abstraction, pending: + # http://projects.puppetlabs.com/issues/19001 + autorequire(:package) do + case value(:provider) + when :iptables_chain + %w{iptables iptables-persistent iptables-services} + else + [] + end + end + + validate do + debug("[validate]") + + value(:name).match(Nameformat) + chain = $1 + table = $2 + protocol = $3 + + # Check that we're not removing an internal chain + if chain =~ InternalChains && value(:ensure) == :absent + self.fail "Cannot remove in-built chains" + end + + if value(:policy).nil? && protocol == 'ethernet' + self.fail "you must set a non-empty policy on all ethernet table chains" + end + + # Check that we're not setting a policy on a user chain + if chain !~ InternalChains && + !value(:policy).nil? && + protocol != 'ethernet' + + self.fail "policy can only be set on in-built chains (with the exception of ethernet chains) (table:#{table} chain:#{chain} protocol:#{protocol})" + end + + # no DROP policy on nat table + if table == 'nat' && + value(:policy) == :drop + + self.fail 'The "nat" table is not intended for filtering, the use of DROP is therefore inhibited' + end + end + + def generate + return [] unless self.purge? + + value(:name).match(Nameformat) + chain = $1 + table = $2 + protocol = $3 + + provider = case protocol + when 'IPv4' + :iptables + when 'IPv6' + :ip6tables + end + + # gather a list of all rules present on the system + rules_resources = Puppet::Type.type(:firewall).instances + + # Keep only rules in this chain + rules_resources.delete_if { |res| (res[:provider] != provider or res.provider.properties[:table].to_s != table or res.provider.properties[:chain] != chain) } + + # Remove rules which match our ignore filter + rules_resources.delete_if {|res| value(:ignore).find_index{|f| res.provider.properties[:line].match(f)}} if value(:ignore) + + # We mark all remaining rules for deletion, and then let the catalog override us on rules which should be present + rules_resources.each {|res| res[:ensure] = :absent} + + rules_resources + end +end diff --git a/firewall/lib/puppet/util/firewall.rb b/firewall/lib/puppet/util/firewall.rb new file mode 100644 index 000000000..9982bed83 --- /dev/null +++ b/firewall/lib/puppet/util/firewall.rb @@ -0,0 +1,226 @@ +require 'socket' +require 'resolv' +require 'puppet/util/ipcidr' + +# Util module for puppetlabs-firewall +module Puppet::Util::Firewall + # Translate the symbolic names for icmp packet types to integers + def icmp_name_to_number(value_icmp, protocol) + if value_icmp =~ /\d{1,2}$/ + value_icmp + elsif protocol == 'inet' + case value_icmp + when "echo-reply" then "0" + when "destination-unreachable" then "3" + when "source-quench" then "4" + when "redirect" then "6" + when "echo-request" then "8" + when "router-advertisement" then "9" + when "router-solicitation" then "10" + when "time-exceeded" then "11" + when "parameter-problem" then "12" + when "timestamp-request" then "13" + when "timestamp-reply" then "14" + when "address-mask-request" then "17" + when "address-mask-reply" then "18" + else nil + end + elsif protocol == 'inet6' + case value_icmp + when "destination-unreachable" then "1" + when "time-exceeded" then "3" + when "parameter-problem" then "4" + when "echo-request" then "128" + when "echo-reply" then "129" + when "router-solicitation" then "133" + when "router-advertisement" then "134" + when "redirect" then "137" + else nil + end + else + raise ArgumentError, "unsupported protocol family '#{protocol}'" + end + end + + # Convert log_level names to their respective numbers + def log_level_name_to_number(value) + #TODO make this 0-7 only + if value =~ /\d/ + value + else + case value + when "panic" then "0" + when "alert" then "1" + when "crit" then "2" + when "err" then "3" + when "error" then "3" + when "warn" then "4" + when "warning" then "4" + when "not" then "5" + when "notice" then "5" + when "info" then "6" + when "debug" then "7" + else nil + end + end + end + + # This method takes a string and a protocol and attempts to convert + # it to a port number if valid. + # + # If the string already contains a port number or perhaps a range of ports + # in the format 22:1000 for example, it simply returns the string and does + # nothing. + def string_to_port(value, proto) + proto = proto.to_s + unless proto =~ /^(tcp|udp)$/ + proto = 'tcp' + end + + m = value.to_s.match(/^(!\s+)?(\S+)/) + if m[2].match(/^\d+(-\d+)?$/) + return "#{m[1]}#{m[2]}" + else + return "#{m[1]}#{Socket.getservbyname(m[2], proto).to_s}" + end + end + + # Takes an address and returns it in CIDR notation. + # + # If the address is: + # + # - A hostname: + # It will be resolved + # - An IPv4 address: + # It will be qualified with a /32 CIDR notation + # - An IPv6 address: + # It will be qualified with a /128 CIDR notation + # - An IP address with a CIDR notation: + # It will be normalised + # - An IP address with a dotted-quad netmask: + # It will be converted to CIDR notation + # - Any address with a resulting prefix length of zero: + # It will return nil which is equivilent to not specifying an address + # + def host_to_ip(value) + begin + value = Puppet::Util::IPCidr.new(value) + rescue + value = Puppet::Util::IPCidr.new(Resolv.getaddress(value)) + end + + return nil if value.prefixlen == 0 + value.cidr + end + + # Takes an address mask and converts the host portion to CIDR notation. + # + # This takes into account you can negate a mask but follows all rules + # defined in host_to_ip for the host/address part. + # + def host_to_mask(value) + match = value.match /(!)\s?(.*)$/ + return host_to_ip(value) unless match + + cidr = host_to_ip(match[2]) + return nil if cidr == nil + "#{match[1]} #{cidr}" + end + + # Validates the argument is int or hex, and returns valid hex + # conversion of the value or nil otherwise. + def to_hex32(value) + begin + value = Integer(value) + if value.between?(0, 0xffffffff) + return '0x' + value.to_s(16) + end + rescue ArgumentError + # pass + end + return nil + end + + def persist_iptables(proto) + debug("[persist_iptables]") + + # Basic normalisation for older Facter + os_key = Facter.value(:osfamily) + os_key ||= case Facter.value(:operatingsystem) + when 'RedHat', 'CentOS', 'Fedora', 'Scientific', 'SL', 'SLC', 'Ascendos', 'CloudLinux', 'PSBM', 'OracleLinux', 'OVS', 'OEL', 'Amazon', 'XenServer' + 'RedHat' + when 'Debian', 'Ubuntu' + 'Debian' + else + Facter.value(:operatingsystem) + end + + # Older iptables-persistent doesn't provide save action. + if os_key == 'Debian' + persist_ver = Facter.value(:iptables_persistent_version) + if (persist_ver and Puppet::Util::Package.versioncmp(persist_ver, '0.5.0') < 0) + os_key = 'Debian_manual' + end + end + + # Fedora 15 and newer use systemd to persist iptable rules + if os_key == 'RedHat' && Facter.value(:operatingsystem) == 'Fedora' && Facter.value(:operatingsystemrelease).to_i >= 15 + os_key = 'Fedora' + end + + # RHEL 7 and newer also use systemd to persist iptable rules + if os_key == 'RedHat' && Facter.value(:operatingsystem) == 'RedHat' && Facter.value(:operatingsystemrelease).to_i >= 7 + os_key = 'Fedora' + end + + cmd = case os_key.to_sym + when :RedHat + case proto.to_sym + when :IPv4 + %w{/sbin/service iptables save} + when :IPv6 + %w{/sbin/service ip6tables save} + end + when :Fedora + case proto.to_sym + when :IPv4 + %w{/usr/libexec/iptables/iptables.init save} + when :IPv6 + %w{/usr/libexec/iptables/ip6tables.init save} + end + when :Debian + case proto.to_sym + when :IPv4, :IPv6 + if Puppet::Util::Package.versioncmp(persist_ver, '1.0') > 0 + %w{/usr/sbin/service netfilter-persistent save} + else + %w{/usr/sbin/service iptables-persistent save} + end + end + when :Debian_manual + case proto.to_sym + when :IPv4 + ["/bin/sh", "-c", "/sbin/iptables-save > /etc/iptables/rules"] + end + when :Archlinux + case proto.to_sym + when :IPv4 + ["/bin/sh", "-c", "/usr/sbin/iptables-save > /etc/iptables/iptables.rules"] + when :IPv6 + ["/bin/sh", "-c", "/usr/sbin/ip6tables-save > /etc/iptables/ip6tables.rules"] + end + end + + # Catch unsupported OSs from the case statement above. + if cmd.nil? + debug('firewall: Rule persistence is not supported for this type/OS') + return + end + + begin + execute(cmd) + rescue Puppet::ExecutionFailure => detail + warning("Unable to persist firewall rules: #{detail}") + end + end +end diff --git a/firewall/lib/puppet/util/ipcidr.rb b/firewall/lib/puppet/util/ipcidr.rb new file mode 100644 index 000000000..87e8d5e37 --- /dev/null +++ b/firewall/lib/puppet/util/ipcidr.rb @@ -0,0 +1,42 @@ +require 'ipaddr' + +# IPCidr object wrapper for IPAddr +module Puppet + module Util + class IPCidr < IPAddr + def initialize(ipaddr) + begin + super(ipaddr) + rescue ArgumentError => e + if e.message =~ /invalid address/ + raise ArgumentError, "Invalid address from IPAddr.new: #{ipaddr}" + else + raise e + end + end + end + + def netmask + _to_string(@mask_addr) + end + + def prefixlen + m = case @family + when Socket::AF_INET + IN4MASK + when Socket::AF_INET6 + IN6MASK + else + raise "unsupported address family" + end + return $1.length if /\A(1*)(0*)\z/ =~ (@mask_addr & m).to_s(2) + raise "bad addr_mask format" + end + + def cidr + cidr = sprintf("%s/%s", self.to_s, self.prefixlen) + cidr + end + end + end +end diff --git a/firewall/manifests/init.pp b/firewall/manifests/init.pp new file mode 100644 index 000000000..759f32823 --- /dev/null +++ b/firewall/manifests/init.pp @@ -0,0 +1,36 @@ +# = Class: firewall +# +# Manages packages and services required by the firewall type/provider. +# +# This class includes the appropriate sub-class for your operating system, +# where supported. +# +# == Parameters: +# +# [*ensure*] +# Ensure parameter passed onto Service[] resources. +# Default: running +# +class firewall ( + $ensure = running +) { + case $ensure { + /^(running|stopped)$/: { + # Do nothing. + } + default: { + fail("${title}: Ensure value '${ensure}' is not supported") + } + } + + case $::kernel { + 'Linux': { + class { "${title}::linux": + ensure => $ensure, + } + } + default: { + fail("${title}: Kernel '${::kernel}' is not currently supported") + } + } +} diff --git a/firewall/manifests/linux.pp b/firewall/manifests/linux.pp new file mode 100644 index 000000000..7c4f3a80b --- /dev/null +++ b/firewall/manifests/linux.pp @@ -0,0 +1,51 @@ +# = Class: firewall::linux +# +# Installs the `iptables` package for Linux operating systems and includes +# the appropriate sub-class for any distribution specific services and +# additional packages. +# +# == Parameters: +# +# [*ensure*] +# Ensure parameter passed onto Service[] resources. When `running` the +# service will be started on boot, and when `stopped` it will not. +# Default: running +# +class firewall::linux ( + $ensure = running +) { + $enable = $ensure ? { + running => true, + stopped => false, + } + + package { 'iptables': + ensure => present, + } + + case $::operatingsystem { + 'RedHat', 'CentOS', 'Fedora', 'Scientific', 'SL', 'SLC', 'Ascendos', + 'CloudLinux', 'PSBM', 'OracleLinux', 'OVS', 'OEL', 'Amazon', 'XenServer': { + class { "${title}::redhat": + ensure => $ensure, + enable => $enable, + require => Package['iptables'], + } + } + 'Debian', 'Ubuntu': { + class { "${title}::debian": + ensure => $ensure, + enable => $enable, + require => Package['iptables'], + } + } + 'Archlinux': { + class { "${title}::archlinux": + ensure => $ensure, + enable => $enable, + require => Package['iptables'], + } + } + default: {} + } +} diff --git a/firewall/manifests/linux/archlinux.pp b/firewall/manifests/linux/archlinux.pp new file mode 100644 index 000000000..546a5a80f --- /dev/null +++ b/firewall/manifests/linux/archlinux.pp @@ -0,0 +1,41 @@ +# = Class: firewall::linux::archlinux +# +# Manages `iptables` and `ip6tables` services, and creates files used for +# persistence, on Arch Linux systems. +# +# == Parameters: +# +# [*ensure*] +# Ensure parameter passed onto Service[] resources. +# Default: running +# +# [*enable*] +# Enable parameter passed onto Service[] resources. +# Default: true +# +class firewall::linux::archlinux ( + $ensure = 'running', + $enable = true +) { + service { 'iptables': + ensure => $ensure, + enable => $enable, + hasstatus => true, + } + + service { 'ip6tables': + ensure => $ensure, + enable => $enable, + hasstatus => true, + } + + file { '/etc/iptables/iptables.rules': + ensure => present, + before => Service['iptables'], + } + + file { '/etc/iptables/ip6tables.rules': + ensure => present, + before => Service['ip6tables'], + } +} diff --git a/firewall/manifests/linux/debian.pp b/firewall/manifests/linux/debian.pp new file mode 100644 index 000000000..4d28bc482 --- /dev/null +++ b/firewall/manifests/linux/debian.pp @@ -0,0 +1,44 @@ +# = Class: firewall::linux::debian +# +# Installs the `iptables-persistent` package for Debian-alike systems. This +# allows rules to be stored to file and restored on boot. +# +# == Parameters: +# +# [*ensure*] +# Ensure parameter passed onto Service[] resources. +# Default: running +# +# [*enable*] +# Enable parameter passed onto Service[] resources. +# Default: true +# +class firewall::linux::debian ( + $ensure = running, + $enable = true +) { + package { 'iptables-persistent': + ensure => present, + } + + if($::operatingsystemrelease =~ /^6\./ and $enable == true + and versioncmp($::iptables_persistent_version, '0.5.0') < 0 ) { + # This fixes a bug in the iptables-persistent LSB headers in 6.x, without it + # we lose idempotency + exec { 'iptables-persistent-enable': + logoutput => on_failure, + command => '/usr/sbin/update-rc.d iptables-persistent enable', + unless => '/usr/bin/test -f /etc/rcS.d/S*iptables-persistent', + require => Package['iptables-persistent'], + } + } else { + # This isn't a real service/daemon. The start action loads rules, so just + # needs to be called on system boot. + service { 'iptables-persistent': + ensure => undef, + enable => $enable, + hasstatus => true, + require => Package['iptables-persistent'], + } + } +} diff --git a/firewall/manifests/linux/redhat.pp b/firewall/manifests/linux/redhat.pp new file mode 100644 index 000000000..b7a4d0e3f --- /dev/null +++ b/firewall/manifests/linux/redhat.pp @@ -0,0 +1,49 @@ +# = Class: firewall::linux::redhat +# +# Manages the `iptables` service on RedHat-alike systems. +# +# == Parameters: +# +# [*ensure*] +# Ensure parameter passed onto Service[] resources. +# Default: running +# +# [*enable*] +# Enable parameter passed onto Service[] resources. +# Default: true +# +class firewall::linux::redhat ( + $ensure = running, + $enable = true +) { + + # RHEL 7 and later and Fedora 15 and later require the iptables-services + # package, which provides the /usr/libexec/iptables/iptables.init used by + # lib/puppet/util/firewall.rb. + if ($::operatingsystem != 'Fedora' and versioncmp($::operatingsystemrelease, '7.0') >= 0) + or ($::operatingsystem == 'Fedora' and versioncmp($::operatingsystemrelease, '15') >= 0) { + package { 'firewalld': + ensure => absent, + before => Package['iptables-services'], + } + + package { 'iptables-services': + ensure => present, + before => Service['iptables'], + } + } + + service { 'iptables': + ensure => $ensure, + enable => $enable, + hasstatus => true, + require => File['/etc/sysconfig/iptables'], + } + + file { '/etc/sysconfig/iptables': + ensure => present, + owner => 'root', + group => 'root', + mode => '0600', + } +} diff --git a/firewall/metadata.json b/firewall/metadata.json new file mode 100644 index 000000000..561891bd5 --- /dev/null +++ b/firewall/metadata.json @@ -0,0 +1,78 @@ +{ + "name": "puppetlabs-firewall", + "version": "1.1.3", + "author": "Puppet Labs", + "summary": "Manages Firewalls such as iptable", + "license": "Apache-2.0", + "source": "https://github.com/puppetlabs/puppetlabs-firewall", + "project_page": "http://forge.puppetlabs.com/puppetlabs/firewall", + "issues_url": "https://github.com/puppetlabs/puppetlabs-firewall/issues", + "operatingsystem_support": [ + { + "operatingsystem": "RedHat", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "CentOS", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "OracleLinux", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "Scientific", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "SLES", + "operatingsystemrelease": [ + "11 SP1" + ] + }, + { + "operatingsystem": "Debian", + "operatingsystemrelease": [ + "6", + "7" + ] + }, + { + "operatingsystem": "Ubuntu", + "operatingsystemrelease": [ + "10.04", + "12.04", + "14.04" + ] + } + ], + "requirements": [ + { + "name": "pe", + "version_requirement": ">= 3.2.0 < 3.4.0" + }, + { + "name": "puppet", + "version_requirement": "3.x" + } + ], + "dependencies": [ + + ] +} diff --git a/firewall/spec/acceptance/change_source_spec.rb b/firewall/spec/acceptance/change_source_spec.rb new file mode 100644 index 000000000..f59110870 --- /dev/null +++ b/firewall/spec/acceptance/change_source_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper_acceptance' + +describe 'firewall type', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + describe 'reset' do + it 'deletes all rules' do + shell('iptables --flush; iptables -t nat --flush; iptables -t mangle --flush') + end + end + + describe 'when unmanaged rules exist' do + it 'applies with 8.0.0.1 first' do + pp = <<-EOS + class { '::firewall': } + firewall { '101 test source changes': + proto => tcp, + port => '101', + action => accept, + source => '8.0.0.1', + } + firewall { '100 test source static': + proto => tcp, + port => '100', + action => accept, + source => '8.0.0.2', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'adds a unmanaged rule without a comment' do + shell('iptables -A INPUT -t filter -s 8.0.0.3/32 -p tcp -m multiport --ports 102 -j ACCEPT') + expect(shell('iptables-save').stdout).to match(/-A INPUT -s 8\.0\.0\.3(\/32)? -p tcp -m multiport --ports 102 -j ACCEPT/) + end + + it 'contains the changable 8.0.0.1 rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.1(\/32)? -p tcp -m multiport --ports 101 -m comment --comment "101 test source changes" -j ACCEPT/) + end + end + it 'contains the static 8.0.0.2 rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.2(\/32)? -p tcp -m multiport --ports 100 -m comment --comment "100 test source static" -j ACCEPT/) + end + end + + it 'changes to 8.0.0.4 second' do + pp = <<-EOS + class { '::firewall': } + firewall { '101 test source changes': + proto => tcp, + port => '101', + action => accept, + source => '8.0.0.4', + } + EOS + + expect(apply_manifest(pp, :catch_failures => true).stdout).to match(/Notice: \/Stage\[main\]\/Main\/Firewall\[101 test source changes\]\/source: source changed '8\.0\.0\.1\/32' to '8\.0\.0\.4\/32'/) + end + + it 'does not contain the old changing 8.0.0.1 rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/8\.0\.0\.1/) + end + end + it 'contains the staic 8.0.0.2 rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.2(\/32)? -p tcp -m multiport --ports 100 -m comment --comment "100 test source static" -j ACCEPT/) + end + end + it 'contains the changing new 8.0.0.4 rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.4(\/32)? -p tcp -m multiport --ports 101 -m comment --comment "101 test source changes" -j ACCEPT/) + end + end + end +end diff --git a/firewall/spec/acceptance/class_spec.rb b/firewall/spec/acceptance/class_spec.rb new file mode 100644 index 000000000..4a9751a6c --- /dev/null +++ b/firewall/spec/acceptance/class_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper_acceptance' + +describe "firewall class:", :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + it 'should run successfully' do + pp = "class { 'firewall': }" + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + + it 'ensure => stopped:' do + pp = "class { 'firewall': ensure => stopped }" + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + + it 'ensure => running:' do + pp = "class { 'firewall': ensure => running }" + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end +end diff --git a/firewall/spec/acceptance/connlimit_spec.rb b/firewall/spec/acceptance/connlimit_spec.rb new file mode 100644 index 000000000..bb049a9e0 --- /dev/null +++ b/firewall/spec/acceptance/connlimit_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper_acceptance' + +describe 'firewall type', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + + describe 'connlimit_above' do + context '10' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '500 - test': + proto => tcp, + dport => '22', + connlimit_above => '10', + action => reject, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + #connlimit-saddr is added in Ubuntu 14.04. + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --dports 22 -m comment --comment "500 - test" -m connlimit --connlimit-above 10 --connlimit-mask 32 (--connlimit-saddr )?-j REJECT --reject-with icmp-port-unreachable/) + end + end + end + end + + describe 'connlimit_mask' do + context '24' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '501 - test': + proto => tcp, + dport => '22', + connlimit_above => '10', + connlimit_mask => '24', + action => reject, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + #connlimit-saddr is added in Ubuntu 14.04. + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --dports 22 -m comment --comment "501 - test" -m connlimit --connlimit-above 10 --connlimit-mask 24 (--connlimit-saddr )?-j REJECT --reject-with icmp-port-unreachable/) + end + end + end + end +end diff --git a/firewall/spec/acceptance/connmark_spec.rb b/firewall/spec/acceptance/connmark_spec.rb new file mode 100644 index 000000000..b3409ab2d --- /dev/null +++ b/firewall/spec/acceptance/connmark_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper_acceptance' + +describe 'firewall type', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + + describe 'connmark' do + context '50' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '502 - test': + proto => 'all', + connmark => '0x1', + action => reject, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -m comment --comment "502 - test" -m connmark --mark 0x1 -j REJECT --reject-with icmp-port-unreachable/) + end + end + end + end +end diff --git a/firewall/spec/acceptance/firewall_spec.rb b/firewall/spec/acceptance/firewall_spec.rb new file mode 100644 index 000000000..bb508d9ae --- /dev/null +++ b/firewall/spec/acceptance/firewall_spec.rb @@ -0,0 +1,1642 @@ +require 'spec_helper_acceptance' + +describe 'firewall type', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + + describe 'reset' do + it 'deletes all rules' do + shell('iptables --flush; iptables -t nat --flush; iptables -t mangle --flush') + end + end + + describe 'name' do + context 'valid' do + it 'applies cleanly' do + pp = <<-EOS + class { '::firewall': } + firewall { '001 - test': ensure => present } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + end + + context 'invalid' do + it 'fails' do + pp = <<-EOS + class { '::firewall': } + firewall { 'test': ensure => present } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/Invalid value "test"./) + end + end + end + end + + describe 'ensure' do + context 'default' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '555 - test': + proto => tcp, + port => '555', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 555 -m comment --comment "555 - test" -j ACCEPT/) + end + end + end + + context 'present' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '555 - test': + ensure => present, + proto => tcp, + port => '555', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 555 -m comment --comment "555 - test" -j ACCEPT/) + end + end + end + + context 'absent' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '555 - test': + ensure => absent, + proto => tcp, + port => '555', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m multiport --ports 555 -m comment --comment "555 - test" -j ACCEPT/) + end + end + end + end + + describe 'source' do + context '192.168.2.0/24' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '556 - test': + proto => tcp, + port => '556', + action => accept, + source => '192.168.2.0/24', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -s 192.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --ports 556 -m comment --comment "556 - test" -j ACCEPT/) + end + end + end + + context '! 192.168.2.0/24' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '556 - test': + proto => tcp, + port => '556', + action => accept, + source => '! 192.168.2.0/24', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT (! -s|-s !) 192.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --ports 556 -m comment --comment "556 - test" -j ACCEPT/) + end + end + end + + # Invalid address + context '256.168.2.0/24' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '556 - test': + proto => tcp, + port => '556', + action => accept, + source => '256.168.2.0/24', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/host_to_ip failed for 256.168.2.0\/(24|255\.255\.255\.0)/) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -s 256.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --ports 556 -m comment --comment "556 - test" -j ACCEPT/) + end + end + end + end + + describe 'src_range' do + context '192.168.1.1-192.168.1.10' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '557 - test': + proto => tcp, + port => '557', + action => accept, + src_range => '192.168.1.1-192.168.1.10', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m iprange --src-range 192.168.1.1-192.168.1.10 -m multiport --ports 557 -m comment --comment "557 - test" -j ACCEPT/) + end + end + end + + # Invalid IP + context '392.168.1.1-192.168.1.10' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '557 - test': + proto => tcp, + port => '557', + action => accept, + src_range => '392.168.1.1-192.168.1.10', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/Invalid value "392.168.1.1-192.168.1.10"/) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m iprange --src-range 392.168.1.1-192.168.1.10 -m multiport --ports 557 -m comment --comment "557 - test" -j ACCEPT/) + end + end + end + end + + describe 'destination' do + context '192.168.2.0/24' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '558 - test': + proto => tcp, + port => '558', + action => accept, + destination => '192.168.2.0/24', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -d 192.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --ports 558 -m comment --comment "558 - test" -j ACCEPT/) + end + end + end + + context '! 192.168.2.0/24' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '558 - test': + proto => tcp, + port => '558', + action => accept, + destination => '! 192.168.2.0/24', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT (! -d|-d !) 192.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --ports 558 -m comment --comment "558 - test" -j ACCEPT/) + end + end + end + + # Invalid address + context '256.168.2.0/24' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '558 - test': + proto => tcp, + port => '558', + action => accept, + destination => '256.168.2.0/24', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/host_to_ip failed for 256.168.2.0\/(24|255\.255\.255\.0)/) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -d 256.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --ports 558 -m comment --comment "558 - test" -j ACCEPT/) + end + end + end + end + + describe 'dst_range' do + context '192.168.1.1-192.168.1.10' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '559 - test': + proto => tcp, + port => '559', + action => accept, + dst_range => '192.168.1.1-192.168.1.10', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m iprange --dst-range 192.168.1.1-192.168.1.10 -m multiport --ports 559 -m comment --comment "559 - test" -j ACCEPT/) + end + end + end + + # Invalid IP + context '392.168.1.1-192.168.1.10' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '559 - test': + proto => tcp, + port => '559', + action => accept, + dst_range => '392.168.1.1-192.168.1.10', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/Invalid value "392.168.1.1-192.168.1.10"/) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m iprange --dst-range 392.168.1.1-192.168.1.10 -m multiport --ports 559 -m comment --comment "559 - test" -j ACCEPT/) + end + end + end + end + + describe 'sport' do + context 'single port' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '560 - test': + proto => tcp, + sport => '560', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --sports 560 -m comment --comment "560 - test" -j ACCEPT/) + end + end + end + + context 'multiple ports' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '560 - test': + proto => tcp, + sport => '560-561', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --sports 560:561 -m comment --comment "560 - test" -j ACCEPT/) + end + end + end + + context 'invalid ports' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '560 - test': + proto => tcp, + sport => '9999560-561', + action => accept, + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/invalid port\/service `9999560' specified/) + end + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m multiport --sports 9999560-561 -m comment --comment "560 - test" -j ACCEPT/) + end + end + end + end + + describe 'dport' do + context 'single port' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '561 - test': + proto => tcp, + dport => '561', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --dports 561 -m comment --comment "561 - test" -j ACCEPT/) + end + end + end + + context 'multiple ports' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '561 - test': + proto => tcp, + dport => '561-562', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --dports 561:562 -m comment --comment "561 - test" -j ACCEPT/) + end + end + end + + context 'invalid ports' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '561 - test': + proto => tcp, + dport => '9999561-562', + action => accept, + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/invalid port\/service `9999561' specified/) + end + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m multiport --dports 9999561-562 -m comment --comment "560 - test" -j ACCEPT/) + end + end + end + end + + describe 'port' do + context 'single port' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '562 - test': + proto => tcp, + port => '562', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 562 -m comment --comment "562 - test" -j ACCEPT/) + end + end + end + + context 'multiple ports' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '562 - test': + proto => tcp, + port => '562-563', + action => accept, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 562:563 -m comment --comment "562 - test" -j ACCEPT/) + end + end + end + + context 'invalid ports' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '562 - test': + proto => tcp, + port => '9999562-563', + action => accept, + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/invalid port\/service `9999562' specified/) + end + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m multiport --ports 9999562-563 -m comment --comment "562 - test" -j ACCEPT/) + end + end + end + end + + ['dst_type', 'src_type'].each do |type| + describe "#{type}" do + context 'MULTICAST' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '563 - test': + proto => tcp, + action => accept, + #{type} => 'MULTICAST', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m addrtype\s.*\sMULTICAST -m comment --comment "563 - test" -j ACCEPT/) + end + end + end + + context 'BROKEN' do + it 'fails' do + pp = <<-EOS + class { '::firewall': } + firewall { '563 - test': + proto => tcp, + action => accept, + #{type} => 'BROKEN', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/Invalid value "BROKEN"./) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m addrtype\s.*\sBROKEN -m comment --comment "563 - test" -j ACCEPT/) + end + end + end + end + end + + describe 'tcp_flags' do + context 'FIN,SYN ACK' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '564 - test': + proto => tcp, + action => accept, + tcp_flags => 'FIN,SYN ACK', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN ACK -m comment --comment "564 - test" -j ACCEPT/) + end + end + end + end + + describe 'chain' do + context 'INPUT' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '565 - test': + proto => tcp, + action => accept, + chain => 'FORWARD', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A FORWARD -p tcp -m comment --comment "565 - test" -j ACCEPT/) + end + end + end + end + + describe 'table' do + context 'mangle' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '566 - test': + proto => tcp, + action => accept, + table => 'mangle', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save -t mangle') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m comment --comment "566 - test" -j ACCEPT/) + end + end + end + context 'nat' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '566 - test2': + proto => tcp, + action => accept, + table => 'nat', + chain => 'OUTPUT', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should not contain the rule' do + shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(/-A OUTPUT -p tcp -m comment --comment "566 - test2" -j ACCEPT/) + end + end + end + end + + describe 'jump' do + after :all do + iptables_flush_all_tables + expect(shell('iptables -t filter -X TEST').stderr).to eq("") + end + + context 'MARK' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewallchain { 'TEST:filter:IPv4': + ensure => present, + } + firewall { '567 - test': + proto => tcp, + chain => 'INPUT', + jump => 'TEST', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m comment --comment "567 - test" -j TEST/) + end + end + end + + context 'jump and apply' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewallchain { 'TEST:filter:IPv4': + ensure => present, + } + firewall { '568 - test': + proto => tcp, + chain => 'INPUT', + action => 'accept', + jump => 'TEST', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/Only one of the parameters 'action' and 'jump' can be set/) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m comment --comment "568 - test" -j TEST/) + end + end + end + end + + describe 'tosource' do + context '192.168.1.1' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '568 - test': + proto => tcp, + table => 'nat', + chain => 'POSTROUTING', + jump => 'SNAT', + tosource => '192.168.1.1', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(/A POSTROUTING -p tcp -m comment --comment "568 - test" -j SNAT --to-source 192.168.1.1/) + end + end + end + end + + describe 'todest' do + context '192.168.1.1' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '569 - test': + proto => tcp, + table => 'nat', + chain => 'PREROUTING', + jump => 'DNAT', + source => '200.200.200.200', + todest => '192.168.1.1', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(/-A PREROUTING -s 200.200.200.200(\/32)? -p tcp -m comment --comment "569 - test" -j DNAT --to-destination 192.168.1.1/) + end + end + end + end + + describe 'toports' do + context '192.168.1.1' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '570 - test': + proto => icmp, + table => 'nat', + chain => 'PREROUTING', + jump => 'REDIRECT', + toports => '2222', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(/-A PREROUTING -p icmp -m comment --comment "570 - test" -j REDIRECT --to-ports 2222/) + end + end + end + end + + # RHEL5 does not support --random + if default['platform'] !~ /el-5/ + describe 'random' do + context '192.168.1.1' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '570 - test 2': + proto => all, + table => 'nat', + chain => 'POSTROUTING', + jump => 'MASQUERADE', + source => '172.30.0.0/16', + random => true + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should contain the rule' do + shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(/-A POSTROUTING -s 172\.30\.0\.0\/16 -m comment --comment "570 - test 2" -j MASQUERADE --random/) + end + end + end + end + end + + describe 'icmp' do + context 'any' do + it 'fails' do + pp = <<-EOS + class { '::firewall': } + firewall { '571 - test': + proto => icmp, + icmp => 'any', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/This behaviour should be achieved by omitting or undefining the ICMP parameter/) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p icmp -m comment --comment "570 - test" -m icmp --icmp-type 11/) + end + end + end + end + + #iptables version 1.3.5 is not suppored by the ip6tables provider + if default['platform'] !~ /el-5/ + describe 'hop_limit' do + context '5' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '571 - test': + ensure => present, + proto => tcp, + port => '571', + action => accept, + hop_limit => '5', + provider => 'ip6tables', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 571 -m comment --comment "571 - test" -m hl --hl-eq 5 -j ACCEPT/) + end + end + end + + context 'invalid' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '571 - test': + ensure => present, + proto => tcp, + port => '571', + action => accept, + hop_limit => 'invalid', + provider => 'ip6tables', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/Invalid value "invalid"./) + end + end + + it 'should not contain the rule' do + shell('ip6tables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m multiport --ports 571 -m comment --comment "571 - test" -m hl --hl-eq invalid -j ACCEPT/) + end + end + end + end + + describe 'ishasmorefrags' do + context 'true' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '587 - test': + ensure => present, + proto => tcp, + port => '587', + action => accept, + ishasmorefrags => true, + provider => 'ip6tables', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/A INPUT -p tcp -m frag --fragid 0 --fragmore -m multiport --ports 587 -m comment --comment "587 - test" -j ACCEPT/) + end + end + end + + context 'false' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '588 - test': + ensure => present, + proto => tcp, + port => '588', + action => accept, + ishasmorefrags => false, + provider => 'ip6tables', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 588 -m comment --comment "588 - test" -j ACCEPT/) + end + end + end + end + + describe 'islastfrag' do + context 'true' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '589 - test': + ensure => present, + proto => tcp, + port => '589', + action => accept, + islastfrag => true, + provider => 'ip6tables', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m frag --fragid 0 --fraglast -m multiport --ports 589 -m comment --comment "589 - test" -j ACCEPT/) + end + end + end + + context 'false' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '590 - test': + ensure => present, + proto => tcp, + port => '590', + action => accept, + islastfrag => false, + provider => 'ip6tables', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 590 -m comment --comment "590 - test" -j ACCEPT/) + end + end + end + end + + describe 'isfirstfrag' do + context 'true' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '591 - test': + ensure => present, + proto => tcp, + port => '591', + action => accept, + isfirstfrag => true, + provider => 'ip6tables', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m frag --fragid 0 --fragfirst -m multiport --ports 591 -m comment --comment "591 - test" -j ACCEPT/) + end + end + end + + context 'false' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '592 - test': + ensure => present, + proto => tcp, + port => '592', + action => accept, + isfirstfrag => false, + provider => 'ip6tables', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 592 -m comment --comment "592 - test" -j ACCEPT/) + end + end + end + end + end + + describe 'limit' do + context '500/sec' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '572 - test': + ensure => present, + proto => tcp, + port => '572', + action => accept, + limit => '500/sec', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 572 -m comment --comment "572 - test" -m limit --limit 500\/sec -j ACCEPT/) + end + end + end + end + + describe 'burst' do + context '500' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '573 - test': + ensure => present, + proto => tcp, + port => '573', + action => accept, + limit => '500/sec', + burst => '1500', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 573 -m comment --comment "573 - test" -m limit --limit 500\/sec --limit-burst 1500 -j ACCEPT/) + end + end + end + + context 'invalid' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '571 - test': + ensure => present, + proto => tcp, + port => '571', + action => accept, + limit => '500/sec', + burst => '1500/sec', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/Invalid value "1500\/sec"./) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m multiport --ports 573 -m comment --comment "573 - test" -m limit --limit 500\/sec --limit-burst 1500\/sec -j ACCEPT/) + end + end + end + end + + describe 'uid' do + context 'nobody' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '574 - test': + ensure => present, + proto => tcp, + chain => 'OUTPUT', + port => '574', + action => accept, + uid => 'nobody', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A OUTPUT -p tcp -m owner --uid-owner (nobody|\d+) -m multiport --ports 574 -m comment --comment "574 - test" -j ACCEPT/) + end + end + end + end + + describe 'gid' do + context 'root' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '575 - test': + ensure => present, + proto => tcp, + chain => 'OUTPUT', + port => '575', + action => accept, + gid => 'root', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A OUTPUT -p tcp -m owner --gid-owner (root|\d+) -m multiport --ports 575 -m comment --comment "575 - test" -j ACCEPT/) + end + end + end + end + + #iptables version 1.3.5 does not support masks on MARK rules + if default['platform'] !~ /el-5/ + describe 'set_mark' do + context '0x3e8/0xffffffff' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '580 - test': + ensure => present, + chain => 'OUTPUT', + proto => tcp, + port => '580', + jump => 'MARK', + table => 'mangle', + set_mark => '0x3e8/0xffffffff', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save -t mangle') do |r| + expect(r.stdout).to match(/-A OUTPUT -p tcp -m multiport --ports 580 -m comment --comment "580 - test" -j MARK --set-xmark 0x3e8\/0xffffffff/) + end + end + end + end + end + + describe 'pkttype' do + context 'multicast' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '581 - test': + ensure => present, + proto => tcp, + port => '581', + action => accept, + pkttype => 'multicast', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 581 -m pkttype --pkt-type multicast -m comment --comment "581 - test" -j ACCEPT/) + end + end + end + + context 'test' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '582 - test': + ensure => present, + proto => tcp, + port => '582', + action => accept, + pkttype => 'test', + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/Invalid value "test"./) + end + end + + it 'should not contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/-A INPUT -p tcp -m multiport --ports 582 -m pkttype --pkt-type multicast -m comment --comment "582 - test" -j ACCEPT/) + end + end + end + end + + describe 'isfragment' do + context 'true' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '583 - test': + ensure => present, + proto => tcp, + port => '583', + action => accept, + isfragment => true, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -f -m multiport --ports 583 -m comment --comment "583 - test" -j ACCEPT/) + end + end + end + + context 'false' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '584 - test': + ensure => present, + proto => tcp, + port => '584', + action => accept, + isfragment => false, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -p tcp -m multiport --ports 584 -m comment --comment "584 - test" -j ACCEPT/) + end + end + end + end + + # RHEL5/SLES does not support -m socket + describe 'socket', :unless => (default['platform'] =~ /el-5/ or fact('operatingsystem') == 'SLES') do + context 'true' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '585 - test': + ensure => present, + proto => tcp, + port => '585', + action => accept, + chain => 'PREROUTING', + table => 'nat', + socket => true, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(/-A PREROUTING -p tcp -m multiport --ports 585 -m socket -m comment --comment "585 - test" -j ACCEPT/) + end + end + end + + context 'false' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '586 - test': + ensure => present, + proto => tcp, + port => '586', + action => accept, + chain => 'PREROUTING', + table => 'nat', + socket => false, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(/-A PREROUTING -p tcp -m multiport --ports 586 -m comment --comment "586 - test" -j ACCEPT/) + end + end + end + end + + describe 'ipsec_policy' do + context 'ipsec' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '593 - test': + ensure => 'present', + action => 'reject', + chain => 'OUTPUT', + destination => '20.0.0.0/8', + ipsec_dir => 'out', + ipsec_policy => 'ipsec', + proto => 'all', + reject => 'icmp-net-unreachable', + table => 'filter', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A OUTPUT -d 20.0.0.0\/(8|255\.0\.0\.0) -m comment --comment "593 - test" -m policy --dir out --pol ipsec -j REJECT --reject-with icmp-net-unreachable/) + end + end + end + + context 'none' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '594 - test': + ensure => 'present', + action => 'reject', + chain => 'OUTPUT', + destination => '20.0.0.0/8', + ipsec_dir => 'out', + ipsec_policy => 'none', + proto => 'all', + reject => 'icmp-net-unreachable', + table => 'filter', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A OUTPUT -d 20.0.0.0\/(8|255\.0\.0\.0) -m comment --comment "594 - test" -m policy --dir out --pol none -j REJECT --reject-with icmp-net-unreachable/) + end + end + end + end + + describe 'ipsec_dir' do + context 'out' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '595 - test': + ensure => 'present', + action => 'reject', + chain => 'OUTPUT', + destination => '20.0.0.0/8', + ipsec_dir => 'out', + ipsec_policy => 'ipsec', + proto => 'all', + reject => 'icmp-net-unreachable', + table => 'filter', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A OUTPUT -d 20.0.0.0\/(8|255\.0\.0\.0) -m comment --comment "595 - test" -m policy --dir out --pol ipsec -j REJECT --reject-with icmp-net-unreachable/) + end + end + end + + context 'in' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '596 - test': + ensure => 'present', + action => 'reject', + chain => 'INPUT', + destination => '20.0.0.0/8', + ipsec_dir => 'in', + ipsec_policy => 'none', + proto => 'all', + reject => 'icmp-net-unreachable', + table => 'filter', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -d 20.0.0.0\/(8|255\.0\.0\.0) -m comment --comment "596 - test" -m policy --dir in --pol none -j REJECT --reject-with icmp-net-unreachable/) + end + end + end + end + + describe 'recent' do + context 'set' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '597 - test': + ensure => 'present', + chain => 'INPUT', + destination => '30.0.0.0/8', + proto => 'all', + table => 'filter', + recent => 'set', + rdest => true, + rname => 'list1', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + # Mask added as of Ubuntu 14.04. + expect(r.stdout).to match(/-A INPUT -d 30.0.0.0\/(8|255\.0\.0\.0) -m comment --comment "597 - test" -m recent --set --name list1 (--mask 255.255.255.255 )?--rdest/) + end + end + end + + context 'rcheck' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '598 - test': + ensure => 'present', + chain => 'INPUT', + destination => '30.0.0.0/8', + proto => 'all', + table => 'filter', + recent => 'rcheck', + rsource => true, + rname => 'list1', + rseconds => 60, + rhitcount => 5, + rttl => true, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -d 30.0.0.0\/(8|255\.0\.0\.0) -m comment --comment "598 - test" -m recent --rcheck --seconds 60 --hitcount 5 --rttl --name list1 (--mask 255.255.255.255 )?--rsource/) + end + end + end + + context 'update' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '599 - test': + ensure => 'present', + chain => 'INPUT', + destination => '30.0.0.0/8', + proto => 'all', + table => 'filter', + recent => 'update', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -d 30.0.0.0\/(8|255\.0\.0\.0) -m comment --comment "599 - test" -m recent --update/) + end + end + end + + context 'remove' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '600 - test': + ensure => 'present', + chain => 'INPUT', + destination => '30.0.0.0/8', + proto => 'all', + table => 'filter', + recent => 'remove', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -d 30.0.0.0\/(8|255\.0\.0\.0) -m comment --comment "600 - test" -m recent --remove/) + end + end + end + end + + describe 'mac_source' do + context '0A:1B:3C:4D:5E:6F' do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '610 - test': + ensure => present, + source => '10.1.5.28/32', + mac_source => '0A:1B:3C:4D:5E:6F', + chain => 'INPUT', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should contain the rule' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT -s 10.1.5.28\/(32|255\.255\.255\.255) -p tcp -m mac --mac-source 0A:1B:3C:4D:5E:6F -m comment --comment "610 - test"/) + end + end + end + end + + describe 'reset' do + it 'deletes all rules' do + shell('ip6tables --flush') + shell('iptables --flush; iptables -t nat --flush; iptables -t mangle --flush') + end + end + +end diff --git a/firewall/spec/acceptance/firewallchain_spec.rb b/firewall/spec/acceptance/firewallchain_spec.rb new file mode 100644 index 000000000..f70d9cefd --- /dev/null +++ b/firewall/spec/acceptance/firewallchain_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper_acceptance' + +describe 'puppet resource firewallchain command:', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + before :all do + iptables_flush_all_tables + end + describe 'ensure' do + context 'present' do + it 'applies cleanly' do + pp = <<-EOS + firewallchain { 'MY_CHAIN:filter:IPv4': + ensure => present, + } + EOS + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'finds the chain' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/MY_CHAIN/) + end + end + end + + context 'absent' do + it 'applies cleanly' do + pp = <<-EOS + firewallchain { 'MY_CHAIN:filter:IPv4': + ensure => absent, + } + EOS + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'fails to find the chain' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/MY_CHAIN/) + end + end + end + end + + # XXX purge => false is not yet implemented + #context 'adding a firewall rule to a chain:' do + # it 'applies cleanly' do + # pp = <<-EOS + # firewallchain { 'MY_CHAIN:filter:IPv4': + # ensure => present, + # } + # firewall { '100 my rule': + # chain => 'MY_CHAIN', + # action => 'accept', + # proto => 'tcp', + # dport => 5000, + # } + # EOS + # # Run it twice and test for idempotency + # apply_manifest(pp, :catch_failures => true) + # apply_manifest(pp, :catch_changes => true) + # end + #end + + #context 'not purge firewallchain chains:' do + # it 'does not purge the rule' do + # pp = <<-EOS + # firewallchain { 'MY_CHAIN:filter:IPv4': + # ensure => present, + # purge => false, + # before => Resources['firewall'], + # } + # resources { 'firewall': + # purge => true, + # } + # EOS + # # Run it twice and test for idempotency + # apply_manifest(pp, :catch_failures => true) do |r| + # expect(r.stdout).to_not match(/removed/) + # expect(r.stderr).to eq('') + # end + # apply_manifest(pp, :catch_changes => true) + # end + + # it 'still has the rule' do + # pp = <<-EOS + # firewall { '100 my rule': + # chain => 'MY_CHAIN', + # action => 'accept', + # proto => 'tcp', + # dport => 5000, + # } + # EOS + # # Run it twice and test for idempotency + # apply_manifest(pp, :catch_changes => true) + # end + #end + + describe 'policy' do + after :all do + shell('iptables -t filter -P FORWARD ACCEPT') + end + + context 'DROP' do + it 'applies cleanly' do + pp = <<-EOS + firewallchain { 'FORWARD:filter:IPv4': + policy => 'drop', + } + EOS + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'finds the chain' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/FORWARD DROP/) + end + end + end + end +end diff --git a/firewall/spec/acceptance/invert_spec.rb b/firewall/spec/acceptance/invert_spec.rb new file mode 100644 index 000000000..aa04912c5 --- /dev/null +++ b/firewall/spec/acceptance/invert_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper_acceptance' + +describe 'firewall type', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + before(:all) do + iptables_flush_all_tables + end + + context "inverting rules" do + it 'applies' do + pp = <<-EOS + class { '::firewall': } + firewall { '601 disallow esp protocol': + action => 'accept', + proto => '! esp', + } + firewall { '602 drop NEW external website packets with FIN/RST/ACK set and SYN unset': + chain => 'INPUT', + state => 'NEW', + action => 'drop', + proto => 'tcp', + sport => ['! http', '! 443'], + source => '! 10.0.0.0/8', + tcp_flags => '! FIN,SYN,RST,ACK SYN', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should contain the rules' do + shell('iptables-save') do |r| + expect(r.stdout).to match(/-A INPUT ! -p esp -m comment --comment "601 disallow esp protocol" -j ACCEPT/) + expect(r.stdout).to match(/-A INPUT ! -s 10\.0\.0\.0\/8 -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m multiport ! --sports 80,443 -m comment --comment "602 drop NEW external website packets with FIN\/RST\/ACK set and SYN unset" -m state --state NEW -j DROP/) + end + end + end + context "inverting partial array rules" do + it 'raises a failure' do + pp = <<-EOS + class { '::firewall': } + firewall { '603 drop 80,443 traffic': + chain => 'INPUT', + action => 'drop', + proto => 'tcp', + sport => ['! http', '443'], + } + EOS + + apply_manifest(pp, :expect_failures => true) do |r| + expect(r.stderr).to match(/is not prefixed/) + end + end + end +end diff --git a/firewall/spec/acceptance/ip6_fragment_spec.rb b/firewall/spec/acceptance/ip6_fragment_spec.rb new file mode 100644 index 000000000..3e44f8723 --- /dev/null +++ b/firewall/spec/acceptance/ip6_fragment_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper_acceptance' + +if default['platform'] =~ /el-5/ + describe "firewall ip6tables doesn't work on 1.3.5 because --comment is missing", :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + before :all do + ip6tables_flush_all_tables + end + + it "can't use ip6tables" do + pp = <<-EOS + class { '::firewall': } + firewall { '599 - test': + ensure => present, + proto => 'tcp', + provider => 'ip6tables', + } + EOS + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/ip6tables provider is not supported/) + end + end +else + describe 'firewall ishasmorefrags/islastfrag/isfirstfrag properties', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + before :all do + ip6tables_flush_all_tables + end + + shared_examples "is idempotent" do |values, line_match| + it "changes the values to #{values}" do + pp = <<-EOS + class { '::firewall': } + firewall { '599 - test': + ensure => present, + proto => 'tcp', + provider => 'ip6tables', + #{values} + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/#{line_match}/) + end + end + end + shared_examples "doesn't change" do |values, line_match| + it "doesn't change the values to #{values}" do + pp = <<-EOS + class { '::firewall': } + firewall { '599 - test': + ensure => present, + proto => 'tcp', + provider => 'ip6tables', + #{values} + } + EOS + + apply_manifest(pp, :catch_changes => true) + + shell('ip6tables-save') do |r| + expect(r.stdout).to match(/#{line_match}/) + end + end + end + + describe 'adding a rule' do + context 'when unset' do + before :all do + ip6tables_flush_all_tables + end + it_behaves_like 'is idempotent', '', /-A INPUT -p tcp -m comment --comment "599 - test"/ + end + context 'when set to true' do + before :all do + ip6tables_flush_all_tables + end + it_behaves_like "is idempotent", 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', /-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"/ + end + context 'when set to false' do + before :all do + ip6tables_flush_all_tables + end + it_behaves_like "is idempotent", 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', /-A INPUT -p tcp -m comment --comment "599 - test"/ + end + end + describe 'editing a rule' do + context 'when unset or false' do + before :each do + ip6tables_flush_all_tables + shell('ip6tables -A INPUT -p tcp -m comment --comment "599 - test"') + end + context 'and current value is false' do + it_behaves_like "doesn't change", 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', /-A INPUT -p tcp -m comment --comment "599 - test"/ + end + context 'and current value is true' do + it_behaves_like "is idempotent", 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', /-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"/ + end + end + context 'when set to true' do + before :each do + ip6tables_flush_all_tables + shell('ip6tables -A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"') + end + context 'and current value is false' do + it_behaves_like "is idempotent", 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', /-A INPUT -p tcp -m comment --comment "599 - test"/ + end + context 'and current value is true' do + it_behaves_like "doesn't change", 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', /-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"/ + end + end + end + end +end diff --git a/firewall/spec/acceptance/isfragment_spec.rb b/firewall/spec/acceptance/isfragment_spec.rb new file mode 100644 index 000000000..a4b65e76e --- /dev/null +++ b/firewall/spec/acceptance/isfragment_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper_acceptance' + +describe 'firewall isfragment property', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + before :all do + iptables_flush_all_tables + end + + shared_examples "is idempotent" do |value, line_match| + it "changes the value to #{value}" do + pp = <<-EOS + class { '::firewall': } + firewall { '597 - test': + ensure => present, + proto => 'tcp', + #{value} + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + + shell('iptables-save') do |r| + expect(r.stdout).to match(/#{line_match}/) + end + end + end + shared_examples "doesn't change" do |value, line_match| + it "doesn't change the value to #{value}" do + pp = <<-EOS + class { '::firewall': } + firewall { '597 - test': + ensure => present, + proto => 'tcp', + #{value} + } + EOS + + apply_manifest(pp, :catch_changes => true) + + shell('iptables-save') do |r| + expect(r.stdout).to match(/#{line_match}/) + end + end + end + + describe 'adding a rule' do + context 'when unset' do + before :all do + iptables_flush_all_tables + end + it_behaves_like 'is idempotent', '', /-A INPUT -p tcp -m comment --comment "597 - test"/ + end + context 'when set to true' do + before :all do + iptables_flush_all_tables + end + it_behaves_like 'is idempotent', 'isfragment => true,', /-A INPUT -p tcp -f -m comment --comment "597 - test"/ + end + context 'when set to false' do + before :all do + iptables_flush_all_tables + end + it_behaves_like "is idempotent", 'isfragment => false,', /-A INPUT -p tcp -m comment --comment "597 - test"/ + end + end + describe 'editing a rule' do + context 'when unset or false' do + before :each do + iptables_flush_all_tables + shell('iptables -A INPUT -p tcp -m comment --comment "597 - test"') + end + context 'and current value is false' do + it_behaves_like "doesn't change", 'isfragment => false,', /-A INPUT -p tcp -m comment --comment "597 - test"/ + end + context 'and current value is true' do + it_behaves_like "is idempotent", 'isfragment => true,', /-A INPUT -p tcp -f -m comment --comment "597 - test"/ + end + end + context 'when set to true' do + before :each do + iptables_flush_all_tables + shell('iptables -A INPUT -p tcp -f -m comment --comment "597 - test"') + end + context 'and current value is false' do + it_behaves_like "is idempotent", 'isfragment => false,', /-A INPUT -p tcp -m comment --comment "597 - test"/ + end + context 'and current value is true' do + it_behaves_like "doesn't change", 'isfragment => true,', /-A INPUT -p tcp -f -m comment --comment "597 - test"/ + end + end + end +end diff --git a/firewall/spec/acceptance/nodesets/centos-59-x64-pe.yml b/firewall/spec/acceptance/nodesets/centos-59-x64-pe.yml new file mode 100644 index 000000000..3a6470bea --- /dev/null +++ b/firewall/spec/acceptance/nodesets/centos-59-x64-pe.yml @@ -0,0 +1,12 @@ +HOSTS: + centos-59-x64: + roles: + - master + - database + - console + platform: el-5-x86_64 + box : centos-59-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-59-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: pe diff --git a/firewall/spec/acceptance/nodesets/centos-59-x64.yml b/firewall/spec/acceptance/nodesets/centos-59-x64.yml new file mode 100644 index 000000000..2ad90b86a --- /dev/null +++ b/firewall/spec/acceptance/nodesets/centos-59-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-59-x64: + roles: + - master + platform: el-5-x86_64 + box : centos-59-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-59-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/firewall/spec/acceptance/nodesets/centos-64-x64-fusion.yml b/firewall/spec/acceptance/nodesets/centos-64-x64-fusion.yml new file mode 100644 index 000000000..d5166735e --- /dev/null +++ b/firewall/spec/acceptance/nodesets/centos-64-x64-fusion.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-64-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-64-x64-fusion503-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-fusion503-nocm.box + hypervisor : fusion +CONFIG: + type: foss diff --git a/firewall/spec/acceptance/nodesets/centos-64-x64-pe.yml b/firewall/spec/acceptance/nodesets/centos-64-x64-pe.yml new file mode 100644 index 000000000..7d9242f1b --- /dev/null +++ b/firewall/spec/acceptance/nodesets/centos-64-x64-pe.yml @@ -0,0 +1,12 @@ +HOSTS: + centos-64-x64: + roles: + - master + - database + - dashboard + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: pe diff --git a/firewall/spec/acceptance/nodesets/centos-64-x64.yml b/firewall/spec/acceptance/nodesets/centos-64-x64.yml new file mode 100644 index 000000000..05540ed8c --- /dev/null +++ b/firewall/spec/acceptance/nodesets/centos-64-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-64-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/firewall/spec/acceptance/nodesets/centos-65-x64.yml b/firewall/spec/acceptance/nodesets/centos-65-x64.yml new file mode 100644 index 000000000..4e2cb809e --- /dev/null +++ b/firewall/spec/acceptance/nodesets/centos-65-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-65-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-65-x64-vbox436-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-65-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/firewall/spec/acceptance/nodesets/debian-607-x64.yml b/firewall/spec/acceptance/nodesets/debian-607-x64.yml new file mode 100644 index 000000000..4c8be42d0 --- /dev/null +++ b/firewall/spec/acceptance/nodesets/debian-607-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-607-x64: + roles: + - master + platform: debian-6-amd64 + box : debian-607-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-607-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/firewall/spec/acceptance/nodesets/debian-70rc1-x64.yml b/firewall/spec/acceptance/nodesets/debian-70rc1-x64.yml new file mode 100644 index 000000000..19181c123 --- /dev/null +++ b/firewall/spec/acceptance/nodesets/debian-70rc1-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-70rc1-x64: + roles: + - master + platform: debian-7-amd64 + box : debian-70rc1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-70rc1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/firewall/spec/acceptance/nodesets/default.yml b/firewall/spec/acceptance/nodesets/default.yml new file mode 100644 index 000000000..05540ed8c --- /dev/null +++ b/firewall/spec/acceptance/nodesets/default.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-64-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/firewall/spec/acceptance/nodesets/fedora-18-x64.yml b/firewall/spec/acceptance/nodesets/fedora-18-x64.yml new file mode 100644 index 000000000..624b53716 --- /dev/null +++ b/firewall/spec/acceptance/nodesets/fedora-18-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + fedora-18-x64: + roles: + - master + platform: fedora-18-x86_64 + box : fedora-18-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/fedora-18-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/firewall/spec/acceptance/nodesets/sles-11sp1-x64.yml b/firewall/spec/acceptance/nodesets/sles-11sp1-x64.yml new file mode 100644 index 000000000..554c37a50 --- /dev/null +++ b/firewall/spec/acceptance/nodesets/sles-11sp1-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + sles-11sp1-x64: + roles: + - master + platform: sles-11-x86_64 + box : sles-11sp1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/sles-11sp1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/firewall/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml b/firewall/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml new file mode 100644 index 000000000..5ca1514e4 --- /dev/null +++ b/firewall/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-10044-x64: + roles: + - master + platform: ubuntu-10.04-amd64 + box : ubuntu-server-10044-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-10044-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/firewall/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml b/firewall/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml new file mode 100644 index 000000000..d065b304f --- /dev/null +++ b/firewall/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-12042-x64: + roles: + - master + platform: ubuntu-12.04-amd64 + box : ubuntu-server-12042-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/firewall/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml b/firewall/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml new file mode 100644 index 000000000..cba1cd04c --- /dev/null +++ b/firewall/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + ubuntu-server-1404-x64: + roles: + - master + platform: ubuntu-14.04-amd64 + box : puppetlabs/ubuntu-14.04-64-nocm + box_url : https://vagrantcloud.com/puppetlabs/ubuntu-14.04-64-nocm + hypervisor : vagrant +CONFIG: + log_level : debug + type: git diff --git a/firewall/spec/acceptance/params_spec.rb b/firewall/spec/acceptance/params_spec.rb new file mode 100644 index 000000000..93b83ef14 --- /dev/null +++ b/firewall/spec/acceptance/params_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper_acceptance' + +describe "param based tests:", :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + # Takes a hash and converts it into a firewall resource + def pp(params) + name = params.delete('name') || '100 test' + pm = <<-EOS +firewall { '#{name}': + EOS + + params.each do |k,v| + pm += <<-EOS + #{k} => #{v}, + EOS + end + + pm += <<-EOS +} + EOS + pm + end + + it 'test various params', :unless => (default['platform'].match(/el-5/) || fact('operatingsystem') == 'SLES') do + iptables_flush_all_tables + + ppm = pp({ + 'table' => "'raw'", + 'socket' => 'true', + 'chain' => "'PREROUTING'", + 'jump' => 'LOG', + 'log_level' => 'debug', + }) + + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2) + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to be_zero + end + + it 'test log rule' do + iptables_flush_all_tables + + ppm = pp({ + 'name' => '998 log all', + 'proto' => 'all', + 'jump' => 'LOG', + 'log_level' => 'debug', + }) + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2) + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to be_zero + end + + it 'test log rule - changing names' do + iptables_flush_all_tables + + ppm1 = pp({ + 'name' => '004 log all INVALID packets', + 'chain' => 'INPUT', + 'proto' => 'all', + 'ctstate' => 'INVALID', + 'jump' => 'LOG', + 'log_level' => '3', + 'log_prefix' => '"IPTABLES dropped invalid: "', + }) + + ppm2 = pp({ + 'name' => '003 log all INVALID packets', + 'chain' => 'INPUT', + 'proto' => 'all', + 'ctstate' => 'INVALID', + 'jump' => 'LOG', + 'log_level' => '3', + 'log_prefix' => '"IPTABLES dropped invalid: "', + }) + + expect(apply_manifest(ppm1, :catch_failures => true).exit_code).to eq(2) + + ppm = <<-EOS + "\n" + ppm2 + resources { 'firewall': + purge => true, + } + EOS + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2) + end + + it 'test chain - changing names' do + iptables_flush_all_tables + + ppm1 = pp({ + 'name' => '004 with a chain', + 'chain' => 'INPUT', + 'proto' => 'all', + }) + + ppm2 = pp({ + 'name' => '004 with a chain', + 'chain' => 'OUTPUT', + 'proto' => 'all', + }) + + apply_manifest(ppm1, :expect_changes => true) + + ppm = <<-EOS + "\n" + ppm2 + resources { 'firewall': + purge => true, + } + EOS + expect(apply_manifest(ppm2, :expect_failures => true).stderr).to match(/is not supported/) + end + + it 'test log rule - idempotent' do + iptables_flush_all_tables + + ppm1 = pp({ + 'name' => '004 log all INVALID packets', + 'chain' => 'INPUT', + 'proto' => 'all', + 'ctstate' => 'INVALID', + 'jump' => 'LOG', + 'log_level' => '3', + 'log_prefix' => '"IPTABLES dropped invalid: "', + }) + + expect(apply_manifest(ppm1, :catch_failures => true).exit_code).to eq(2) + expect(apply_manifest(ppm1, :catch_failures => true).exit_code).to be_zero + end + + it 'test src_range rule' do + iptables_flush_all_tables + + ppm = pp({ + 'name' => '997 block src ip range', + 'chain' => 'INPUT', + 'proto' => 'all', + 'action' => 'drop', + 'src_range' => '"10.0.0.1-10.0.0.10"', + }) + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2) + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to be_zero + end + + it 'test dst_range rule' do + iptables_flush_all_tables + + ppm = pp({ + 'name' => '998 block dst ip range', + 'chain' => 'INPUT', + 'proto' => 'all', + 'action' => 'drop', + 'dst_range' => '"10.0.0.2-10.0.0.20"', + }) + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2) + expect(apply_manifest(ppm, :catch_failures => true).exit_code).to be_zero + end + +end diff --git a/firewall/spec/acceptance/purge_spec.rb b/firewall/spec/acceptance/purge_spec.rb new file mode 100644 index 000000000..4de968a32 --- /dev/null +++ b/firewall/spec/acceptance/purge_spec.rb @@ -0,0 +1,124 @@ +require 'spec_helper_acceptance' + +describe "purge tests:", :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + context('resources purge') do + before(:all) do + iptables_flush_all_tables + + shell('iptables -A INPUT -s 1.2.1.2') + shell('iptables -A INPUT -s 1.2.1.2') + end + + it 'make sure duplicate existing rules get purged' do + + pp = <<-EOS + class { 'firewall': } + resources { 'firewall': + purge => true, + } + EOS + + apply_manifest(pp, :expect_changes => true) + end + + it 'saves' do + shell('iptables-save') do |r| + expect(r.stdout).to_not match(/1\.2\.1\.2/) + expect(r.stderr).to eq("") + end + end + end + + context('chain purge') do + before(:each) do + iptables_flush_all_tables + + shell('iptables -A INPUT -p tcp -s 1.2.1.1') + shell('iptables -A INPUT -p udp -s 1.2.1.1') + shell('iptables -A OUTPUT -s 1.2.1.2 -m comment --comment "010 output-1.2.1.2"') + end + + it 'purges only the specified chain' do + pp = <<-EOS + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv4': + purge => true, + } + EOS + + apply_manifest(pp, :expect_changes => true) + + shell('iptables-save') do |r| + expect(r.stdout).to match(/010 output-1\.2\.1\.2/) + expect(r.stdout).to_not match(/1\.2\.1\.1/) + expect(r.stderr).to eq("") + end + end + + it 'ignores managed rules' do + pp = <<-EOS + class { 'firewall': } + firewallchain { 'OUTPUT:filter:IPv4': + purge => true, + } + firewall { '010 output-1.2.1.2': + chain => 'OUTPUT', + proto => 'all', + source => '1.2.1.2', + } + EOS + + apply_manifest(pp, :catch_changes => true) + end + + it 'ignores specified rules' do + pp = <<-EOS + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv4': + purge => true, + ignore => [ + '-s 1\.2\.1\.1', + ], + } + EOS + + apply_manifest(pp, :catch_changes => true) + end + + it 'adds managed rules with ignored rules' do + pp = <<-EOS + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv4': + purge => true, + ignore => [ + '-s 1\.2\.1\.1', + ], + } + firewall { '014 input-1.2.1.6': + chain => 'INPUT', + proto => 'all', + source => '1.2.1.6', + } + -> firewall { '013 input-1.2.1.5': + chain => 'INPUT', + proto => 'all', + source => '1.2.1.5', + } + -> firewall { '012 input-1.2.1.4': + chain => 'INPUT', + proto => 'all', + source => '1.2.1.4', + } + -> firewall { '011 input-1.2.1.3': + chain => 'INPUT', + proto => 'all', + source => '1.2.1.3', + } + EOS + + apply_manifest(pp, :catch_failures => true) + + expect(shell('iptables-save').stdout).to match(/-A INPUT -s 1\.2\.1\.1(\/32)? -p tcp\s?\n-A INPUT -s 1\.2\.1\.1(\/32)? -p udp/) + end + end +end diff --git a/firewall/spec/acceptance/resource_cmd_spec.rb b/firewall/spec/acceptance/resource_cmd_spec.rb new file mode 100644 index 000000000..b942c5580 --- /dev/null +++ b/firewall/spec/acceptance/resource_cmd_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper_acceptance' + +# Here we want to test the the resource commands ability to work with different +# existing ruleset scenarios. This will give the parsing capabilities of the +# code a good work out. +describe 'puppet resource firewall command:', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + context 'make sure it returns no errors when executed on a clean machine' do + it do + shell('puppet resource firewall') do |r| + r.exit_code.should be_zero + # don't check stdout, some boxes come with rules, that is normal + # don't check stderr, puppet throws deprecation warnings + end + end + end + + context 'flush iptables and make sure it returns nothing afterwards' do + before(:all) do + iptables_flush_all_tables + end + + # No rules, means no output thanks. And no errors as well. + it do + shell('puppet resource firewall') do |r| + r.exit_code.should be_zero + r.stdout.should == "\n" + end + end + end + + context 'accepts rules without comments' do + before(:all) do + iptables_flush_all_tables + shell('iptables -A INPUT -j ACCEPT -p tcp --dport 80') + end + + it do + shell('puppet resource firewall') do |r| + r.exit_code.should be_zero + # don't check stdout, testing preexisting rules, output is normal + # don't check stderr, puppet throws deprecation warnings + end + end + end + + context 'accepts rules with invalid comments' do + before(:all) do + iptables_flush_all_tables + shell('iptables -A INPUT -j ACCEPT -p tcp --dport 80 -m comment --comment "http"') + end + + it do + shell('puppet resource firewall') do |r| + r.exit_code.should be_zero + # don't check stdout, testing preexisting rules, output is normal + # don't check stderr, puppet throws deprecation warnings + end + end + end + + context 'accepts rules with negation' do + before :all do + iptables_flush_all_tables + shell('iptables -t nat -A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -p tcp -j MASQUERADE --to-ports 1024-65535') + shell('iptables -t nat -A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -p udp -j MASQUERADE --to-ports 1024-65535') + shell('iptables -t nat -A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -j MASQUERADE') + end + + it do + shell('puppet resource firewall') do |r| + r.exit_code.should be_zero + # don't check stdout, testing preexisting rules, output is normal + # don't check stderr, puppet throws deprecation warnings + end + end + end + + context 'accepts rules with match extension tcp flag' do + before :all do + iptables_flush_all_tables + shell('iptables -t mangle -A PREROUTING -d 1.2.3.4 -p tcp -m tcp -m multiport --dports 80,443,8140 -j MARK --set-mark 42') + end + + it do + shell('puppet resource firewall') do |r| + r.exit_code.should be_zero + # don't check stdout, testing preexisting rules, output is normal + # don't check stderr, puppet throws deprecation warnings + end + end + end + + context 'accepts rules utilizing the statistic module' do + before :all do + iptables_flush_all_tables + shell('iptables -t nat -A POSTROUTING -d 1.2.3.4/32 -o eth0 -m statistic --mode nth --every 2 -j SNAT --to-source 2.3.4.5') + shell('iptables -t nat -A POSTROUTING -d 1.2.3.4/32 -o eth0 -m statistic --mode nth --every 1 --packet 0 -j SNAT --to-source 2.3.4.6') + shell('iptables -t nat -A POSTROUTING -d 1.2.3.4/32 -o eth0 -m statistic --mode random --probability 0.99 -j SNAT --to-source 2.3.4.7') + end + + it do + shell('puppet resource firewall') do |r| + r.exit_code.should be_zero + # don't check stdout, testing preexisting rules, output is normal + r.stderr.should be_empty + end + end + end + + context 'accepts rules with negation' do + before :all do + iptables_flush_all_tables + shell('iptables -t nat -A POSTROUTING -s 192.168.122.0/24 -m policy --dir out --pol ipsec -j ACCEPT') + shell('iptables -t filter -A FORWARD -s 192.168.1.0/24 -d 192.168.122.0/24 -i eth0 -m policy --dir in --pol ipsec --reqid 108 --proto esp -j ACCEPT') + shell('iptables -t filter -A FORWARD -s 192.168.122.0/24 -d 192.168.1.0/24 -o eth0 -m policy --dir out --pol ipsec --reqid 108 --proto esp -j ACCEPT') + shell('iptables -t filter -A FORWARD -s 192.168.201.1/32 -d 192.168.122.0/24 -i eth0 -m policy --dir in --pol ipsec --reqid 107 --proto esp -j ACCEPT') + shell('iptables -t filter -A FORWARD -s 192.168.122.0/24 -d 192.168.201.1/32 -o eth0 -m policy --dir out --pol ipsec --reqid 107 --proto esp -j ACCEPT') + end + + it do + shell('puppet resource firewall') do |r| + r.exit_code.should be_zero + # don't check stdout, testing preexisting rules, output is normal + r.stderr.should be_empty + end + end + end +end diff --git a/firewall/spec/acceptance/rules_spec.rb b/firewall/spec/acceptance/rules_spec.rb new file mode 100644 index 000000000..b7eb2df16 --- /dev/null +++ b/firewall/spec/acceptance/rules_spec.rb @@ -0,0 +1,252 @@ +require 'spec_helper_acceptance' + +describe 'complex ruleset 1', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + before :all do + iptables_flush_all_tables + end + + after :all do + shell('iptables -t filter -P INPUT ACCEPT') + shell('iptables -t filter -P FORWARD ACCEPT') + shell('iptables -t filter -P OUTPUT ACCEPT') + shell('iptables -t filter --flush') + end + + it 'applies cleanly' do + pp = <<-EOS + firewall { '090 forward allow local': + chain => 'FORWARD', + proto => 'all', + source => '10.0.0.0/8', + destination => '10.0.0.0/8', + action => 'accept', + } + firewall { '100 forward standard allow tcp': + chain => 'FORWARD', + source => '10.0.0.0/8', + destination => '!10.0.0.0/8', + proto => 'tcp', + state => 'NEW', + port => [80,443,21,20,22,53,123,43,873,25,465], + action => 'accept', + } + firewall { '100 forward standard allow udp': + chain => 'FORWARD', + source => '10.0.0.0/8', + destination => '!10.0.0.0/8', + proto => 'udp', + port => [53,123], + action => 'accept', + } + firewall { '100 forward standard allow icmp': + chain => 'FORWARD', + source => '10.0.0.0/8', + destination => '!10.0.0.0/8', + proto => 'icmp', + action => 'accept', + } + + firewall { '090 ignore ipsec': + table => 'nat', + chain => 'POSTROUTING', + outiface => 'eth0', + ipsec_policy => 'ipsec', + ipsec_dir => 'out', + action => 'accept', + } + firewall { '093 ignore 10.0.0.0/8': + table => 'nat', + chain => 'POSTROUTING', + outiface => 'eth0', + destination => '10.0.0.0/8', + action => 'accept', + } + firewall { '093 ignore 172.16.0.0/12': + table => 'nat', + chain => 'POSTROUTING', + outiface => 'eth0', + destination => '172.16.0.0/12', + action => 'accept', + } + firewall { '093 ignore 192.168.0.0/16': + table => 'nat', + chain => 'POSTROUTING', + outiface => 'eth0', + destination => '192.168.0.0/16', + action => 'accept', + } + firewall { '100 masq outbound': + table => 'nat', + chain => 'POSTROUTING', + outiface => 'eth0', + jump => 'MASQUERADE', + } + firewall { '101 redirect port 1': + table => 'nat', + chain => 'PREROUTING', + iniface => 'eth0', + proto => 'tcp', + dport => '1', + toports => '22', + jump => 'REDIRECT', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end + + it 'contains appropriate rules' do + shell('iptables-save') do |r| + [ + /INPUT ACCEPT/, + /FORWARD ACCEPT/, + /OUTPUT ACCEPT/, + /-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) -d 10.0.0.0\/(8|255\.0\.0\.0) -m comment --comment \"090 forward allow local\" -j ACCEPT/, + /-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p icmp -m comment --comment \"100 forward standard allow icmp\" -j ACCEPT/, + /-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p tcp -m multiport --ports 80,443,21,20,22,53,123,43,873,25,465 -m comment --comment \"100 forward standard allow tcp\" -m state --state NEW -j ACCEPT/, + /-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p udp -m multiport --ports 53,123 -m comment --comment \"100 forward standard allow udp\" -j ACCEPT/ + ].each do |line| + expect(r.stdout).to match(line) + end + end + end +end + +describe 'complex ruleset 2' do + after :all do + shell('iptables -t filter -P INPUT ACCEPT') + shell('iptables -t filter -P FORWARD ACCEPT') + shell('iptables -t filter -P OUTPUT ACCEPT') + shell('iptables -t filter --flush') + expect(shell('iptables -t filter -X LOCAL_INPUT').stderr).to eq("") + expect(shell('iptables -t filter -X LOCAL_INPUT_PRE').stderr).to eq("") + end + + it 'applies cleanly' do + pp = <<-EOS + class { '::firewall': } + + Firewall { + proto => 'all', + stage => 'pre', + } + Firewallchain { + stage => 'pre', + purge => 'true', + ignore => [ + '--comment "[^"]*(?i:ignore)[^"]*"', + ], + } + + firewall { '010 INPUT allow established and related': + proto => 'all', + state => ['ESTABLISHED', 'RELATED'], + action => 'accept', + before => Firewallchain['INPUT:filter:IPv4'], + } + firewall { '012 accept loopback': + iniface => 'lo', + action => 'accept', + before => Firewallchain['INPUT:filter:IPv4'], + } + firewall { '020 ssh': + proto => 'tcp', + dport => '22', + state => 'NEW', + action => 'accept', + before => Firewallchain['INPUT:filter:IPv4'], + } + + firewall { '013 icmp echo-request': + proto => 'icmp', + icmp => 'echo-request', + action => 'accept', + source => '10.0.0.0/8', + } + firewall { '013 icmp destination-unreachable': + proto => 'icmp', + icmp => 'destination-unreachable', + action => 'accept', + } + firewall { '013 icmp time-exceeded': + proto => 'icmp', + icmp => 'time-exceeded', + action => 'accept', + } + firewall { '999 reject': + action => 'reject', + reject => 'icmp-host-prohibited', + } + + + firewallchain { 'LOCAL_INPUT_PRE:filter:IPv4': } + firewall { '001 LOCAL_INPUT_PRE': + jump => 'LOCAL_INPUT_PRE', + require => Firewallchain['LOCAL_INPUT_PRE:filter:IPv4'], + } + firewallchain { 'LOCAL_INPUT:filter:IPv4': } + firewall { '900 LOCAL_INPUT': + jump => 'LOCAL_INPUT', + require => Firewallchain['LOCAL_INPUT:filter:IPv4'], + } + firewallchain { 'INPUT:filter:IPv4': + policy => 'drop', + ignore => [ + '-j fail2ban-ssh', + '--comment "[^"]*(?i:ignore)[^"]*"', + ], + } + + + firewall { '010 allow established and related': + chain => 'FORWARD', + proto => 'all', + state => ['ESTABLISHED','RELATED'], + action => 'accept', + before => Firewallchain['FORWARD:filter:IPv4'], + } + firewallchain { 'FORWARD:filter:IPv4': + policy => 'drop', + } + + firewallchain { 'OUTPUT:filter:IPv4': } + + + # purge unknown rules from mangle table + firewallchain { ['PREROUTING:mangle:IPv4', 'INPUT:mangle:IPv4', 'FORWARD:mangle:IPv4', 'OUTPUT:mangle:IPv4', 'POSTROUTING:mangle:IPv4']: } + + # and the nat table + firewallchain { ['PREROUTING:nat:IPv4', 'INPUT:nat:IPv4', 'OUTPUT:nat:IPv4', 'POSTROUTING:nat:IPv4']: } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'contains appropriate rules' do + shell('iptables-save') do |r| + [ + /INPUT DROP/, + /FORWARD DROP/, + /OUTPUT ACCEPT/, + /LOCAL_INPUT/, + /LOCAL_INPUT_PRE/, + /-A INPUT -m comment --comment \"001 LOCAL_INPUT_PRE\" -j LOCAL_INPUT_PRE/, + /-A INPUT -m comment --comment \"010 INPUT allow established and related\" -m state --state RELATED,ESTABLISHED -j ACCEPT/, + /-A INPUT -i lo -m comment --comment \"012 accept loopback\" -j ACCEPT/, + /-A INPUT -p icmp -m comment --comment \"013 icmp destination-unreachable\" -m icmp --icmp-type 3 -j ACCEPT/, + /-A INPUT -s 10.0.0.0\/(8|255\.0\.0\.0) -p icmp -m comment --comment \"013 icmp echo-request\" -m icmp --icmp-type 8 -j ACCEPT/, + /-A INPUT -p icmp -m comment --comment \"013 icmp time-exceeded\" -m icmp --icmp-type 11 -j ACCEPT/, + /-A INPUT -p tcp -m multiport --dports 22 -m comment --comment \"020 ssh\" -m state --state NEW -j ACCEPT/, + /-A INPUT -m comment --comment \"900 LOCAL_INPUT\" -j LOCAL_INPUT/, + /-A INPUT -m comment --comment \"999 reject\" -j REJECT --reject-with icmp-host-prohibited/, + /-A FORWARD -m comment --comment \"010 allow established and related\" -m state --state RELATED,ESTABLISHED -j ACCEPT/ + ].each do |line| + expect(r.stdout).to match(line) + end + end + end +end diff --git a/firewall/spec/acceptance/socket_spec.rb b/firewall/spec/acceptance/socket_spec.rb new file mode 100644 index 000000000..5503a9a07 --- /dev/null +++ b/firewall/spec/acceptance/socket_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper_acceptance' + +# RHEL5 does not support -m socket +describe 'firewall socket property', :unless => (UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) || default['platform'] =~ /el-5/ || fact('operatingsystem') == 'SLES') do + before :all do + iptables_flush_all_tables + end + + shared_examples "is idempotent" do |value, line_match| + it "changes the value to #{value}" do + pp = <<-EOS + class { '::firewall': } + firewall { '598 - test': + ensure => present, + proto => 'tcp', + chain => 'PREROUTING', + table => 'raw', + #{value} + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + + shell('iptables-save -t raw') do |r| + expect(r.stdout).to match(/#{line_match}/) + end + end + end + shared_examples "doesn't change" do |value, line_match| + it "doesn't change the value to #{value}" do + pp = <<-EOS + class { '::firewall': } + firewall { '598 - test': + ensure => present, + proto => 'tcp', + chain => 'PREROUTING', + table => 'raw', + #{value} + } + EOS + + apply_manifest(pp, :catch_changes => true) + + shell('iptables-save -t raw') do |r| + expect(r.stdout).to match(/#{line_match}/) + end + end + end + + describe 'adding a rule' do + context 'when unset' do + before :all do + iptables_flush_all_tables + end + it_behaves_like 'is idempotent', '', /-A PREROUTING -p tcp -m comment --comment "598 - test"/ + end + context 'when set to true' do + before :all do + iptables_flush_all_tables + end + it_behaves_like 'is idempotent', 'socket => true,', /-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"/ + end + context 'when set to false' do + before :all do + iptables_flush_all_tables + end + it_behaves_like "is idempotent", 'socket => false,', /-A PREROUTING -p tcp -m comment --comment "598 - test"/ + end + end + describe 'editing a rule' do + context 'when unset or false' do + before :each do + iptables_flush_all_tables + shell('iptables -t raw -A PREROUTING -p tcp -m comment --comment "598 - test"') + end + context 'and current value is false' do + it_behaves_like "doesn't change", 'socket => false,', /-A PREROUTING -p tcp -m comment --comment "598 - test"/ + end + context 'and current value is true' do + it_behaves_like "is idempotent", 'socket => true,', /-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"/ + end + end + context 'when set to true' do + before :each do + iptables_flush_all_tables + shell('iptables -t raw -A PREROUTING -p tcp -m socket -m comment --comment "598 - test"') + end + context 'and current value is false' do + it_behaves_like "is idempotent", 'socket => false,', /-A PREROUTING -p tcp -m comment --comment "598 - test"/ + end + context 'and current value is true' do + it_behaves_like "doesn't change", 'socket => true,', /-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"/ + end + end + end +end diff --git a/firewall/spec/acceptance/standard_usage_spec.rb b/firewall/spec/acceptance/standard_usage_spec.rb new file mode 100644 index 000000000..8dcbceff1 --- /dev/null +++ b/firewall/spec/acceptance/standard_usage_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper_acceptance' + +# Some tests for the standard recommended usage +describe 'standard usage tests:', :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + it 'applies twice' do + pp = <<-EOS + class my_fw::pre { + Firewall { + require => undef, + } + + # Default firewall rules + firewall { '000 accept all icmp': + proto => 'icmp', + action => 'accept', + }-> + firewall { '001 accept all to lo interface': + proto => 'all', + iniface => 'lo', + action => 'accept', + }-> + firewall { '002 accept related established rules': + proto => 'all', + ctstate => ['RELATED', 'ESTABLISHED'], + action => 'accept', + } + } + class my_fw::post { + firewall { '999 drop all': + proto => 'all', + action => 'drop', + before => undef, + } + } + resources { "firewall": + purge => true + } + Firewall { + before => Class['my_fw::post'], + require => Class['my_fw::pre'], + } + class { ['my_fw::pre', 'my_fw::post']: } + class { 'firewall': } + firewall { '500 open up port 22': + action => 'accept', + proto => 'tcp', + dport => 22, + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero + end +end diff --git a/firewall/spec/acceptance/unsupported_spec.rb b/firewall/spec/acceptance/unsupported_spec.rb new file mode 100644 index 000000000..dfb75e235 --- /dev/null +++ b/firewall/spec/acceptance/unsupported_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper_acceptance' + +describe 'unsupported distributions and OSes', :if => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + it 'should fail' do + pp = <<-EOS + class { 'firewall': } + EOS + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/not currently supported/i) + end +end diff --git a/firewall/spec/fixtures/ip6tables/conversion_hash.rb b/firewall/spec/fixtures/ip6tables/conversion_hash.rb new file mode 100644 index 000000000..7c507d78b --- /dev/null +++ b/firewall/spec/fixtures/ip6tables/conversion_hash.rb @@ -0,0 +1,107 @@ +# These hashes allow us to iterate across a series of test data +# creating rspec examples for each parameter to ensure the input :line +# extrapolates to the desired value for the parameter in question. And +# vice-versa + +# This hash is for testing a line conversion to a hash of parameters +# which will be used to create a resource. +ARGS_TO_HASH6 = { + 'source_destination_ipv6_no_cidr' => { + :line => '-A INPUT -s 2001:db8:85a3::8a2e:370:7334 -d 2001:db8:85a3::8a2e:370:7334 -m comment --comment "000 source destination ipv6 no cidr"', + :table => 'filter', + :provider => 'ip6tables', + :params => { + :source => '2001:db8:85a3::8a2e:370:7334/128', + :destination => '2001:db8:85a3::8a2e:370:7334/128', + }, + }, + 'source_destination_ipv6_netmask' => { + :line => '-A INPUT -s 2001:db8:1234::/ffff:ffff:ffff:0000:0000:0000:0000:0000 -d 2001:db8:4321::/ffff:ffff:ffff:0000:0000:0000:0000:0000 -m comment --comment "000 source destination ipv6 netmask"', + :table => 'filter', + :provider => 'ip6tables', + :params => { + :source => '2001:db8:1234::/48', + :destination => '2001:db8:4321::/48', + }, + }, +} + +# This hash is for testing converting a hash to an argument line. +HASH_TO_ARGS6 = { + 'zero_prefixlen_ipv6' => { + :params => { + :name => '100 zero prefix length ipv6', + :table => 'filter', + :provider => 'ip6tables', + :source => '::/0', + :destination => '::/0', + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'comment', '--comment', '100 zero prefix length ipv6'], + }, + 'source_destination_ipv4_no_cidr' => { + :params => { + :name => '000 source destination ipv4 no cidr', + :table => 'filter', + :provider => 'ip6tables', + :source => '1.1.1.1', + :destination => '2.2.2.2', + }, + :args => ['-t', :filter, '-s', '1.1.1.1/32', '-d', '2.2.2.2/32', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv4 no cidr'], + }, + 'source_destination_ipv6_no_cidr' => { + :params => { + :name => '000 source destination ipv6 no cidr', + :table => 'filter', + :provider => 'ip6tables', + :source => '2001:db8:1234::', + :destination => '2001:db8:4321::', + }, + :args => ['-t', :filter, '-s', '2001:db8:1234::/128', '-d', '2001:db8:4321::/128', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv6 no cidr'], + }, + 'source_destination_ipv6_netmask' => { + :params => { + :name => '000 source destination ipv6 netmask', + :table => 'filter', + :provider => 'ip6tables', + :source => '2001:db8:1234::/ffff:ffff:ffff:0000:0000:0000:0000:0000', + :destination => '2001:db8:4321::/ffff:ffff:ffff:0000:0000:0000:0000:0000', + }, + :args => ['-t', :filter, '-s', '2001:db8:1234::/48', '-d', '2001:db8:4321::/48', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv6 netmask'], + }, + 'frag_ishasmorefrags' => { + :params => { + :name => "100 has more fragments", + :ishasmorefrags => true, + :provider => 'ip6tables', + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fragmore", "-m", "comment", "--comment", "100 has more fragments"], + }, + 'frag_islastfrag' => { + :params => { + :name => "100 last fragment", + :islastfrag => true, + :provider => 'ip6tables', + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fraglast", "-m", "comment", "--comment", "100 last fragment"], + }, + 'frag_isfirstfrags' => { + :params => { + :name => "100 first fragment", + :isfirstfrag => true, + :provider => 'ip6tables', + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fragfirst", "-m", "comment", "--comment", "100 first fragment"], + }, + 'hop_limit' => { + :params => { + :name => "100 hop limit", + :hop_limit => 255, + :provider => 'ip6tables', + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment", "100 hop limit", "-m", "hl", "--hl-eq", 255], + }, +} diff --git a/firewall/spec/fixtures/iptables/conversion_hash.rb b/firewall/spec/fixtures/iptables/conversion_hash.rb new file mode 100644 index 000000000..3024e80d5 --- /dev/null +++ b/firewall/spec/fixtures/iptables/conversion_hash.rb @@ -0,0 +1,1013 @@ +# These hashes allow us to iterate across a series of test data +# creating rspec examples for each parameter to ensure the input :line +# extrapolates to the desired value for the parameter in question. And +# vice-versa + +# This hash is for testing a line conversion to a hash of parameters +# which will be used to create a resource. +ARGS_TO_HASH = { + 'mac_source_1' => { + :line => '-A neutron-openvswi-FORWARD -s 1.2.3.4/32 -m mac --mac-source FA:16:00:00:00:00 -j ACCEPT', + :table => 'filter', + :params => { + :chain => 'neutron-openvswi-FORWARD', + :source => '1.2.3.4/32', + :mac_source => 'FA:16:00:00:00:00', + }, + }, + 'dport_and_sport' => { + :line => '-A nova-compute-FORWARD -s 0.0.0.0/32 -d 255.255.255.255/32 -p udp -m udp --sport 68 --dport 67 -j ACCEPT', + :table => 'filter', + :params => { + :action => 'accept', + :chain => 'nova-compute-FORWARD', + :source => '0.0.0.0/32', + :destination => '255.255.255.255/32', + :sport => ['68'], + :dport => ['67'], + :proto => 'udp', + }, + }, + 'long_rule_1' => { + :line => '-A INPUT -s 1.1.1.1/32 -d 1.1.1.1/32 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -m comment --comment "000 allow foo" -j ACCEPT', + :table => 'filter', + :compare_all => true, + :params => { + :action => "accept", + :chain => "INPUT", + :destination => "1.1.1.1/32", + :dport => ["7061","7062"], + :ensure => :present, + :line => '-A INPUT -s 1.1.1.1/32 -d 1.1.1.1/32 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -m comment --comment "000 allow foo" -j ACCEPT', + :name => "000 allow foo", + :proto => "tcp", + :provider => "iptables", + :source => "1.1.1.1/32", + :sport => ["7061","7062"], + :table => "filter", + }, + }, + 'action_drop_1' => { + :line => '-A INPUT -m comment --comment "000 allow foo" -j DROP', + :table => 'filter', + :params => { + :jump => nil, + :action => "drop", + }, + }, + 'action_reject_1' => { + :line => '-A INPUT -m comment --comment "000 allow foo" -j REJECT', + :table => 'filter', + :params => { + :jump => nil, + :action => "reject", + }, + }, + 'action_nil_1' => { + :line => '-A INPUT -m comment --comment "000 allow foo"', + :table => 'filter', + :params => { + :jump => nil, + :action => nil, + }, + }, + 'jump_custom_chain_1' => { + :line => '-A INPUT -m comment --comment "000 allow foo" -j custom_chain', + :table => 'filter', + :params => { + :jump => "custom_chain", + :action => nil, + }, + }, + 'source_destination_ipv4_no_cidr' => { + :line => '-A INPUT -s 1.1.1.1 -d 2.2.2.2 -m comment --comment "000 source destination ipv4 no cidr"', + :table => 'filter', + :params => { + :source => '1.1.1.1/32', + :destination => '2.2.2.2/32', + }, + }, + 'source_destination_ipv6_no_cidr' => { + :line => '-A INPUT -s 2001:db8:85a3::8a2e:370:7334 -d 2001:db8:85a3::8a2e:370:7334 -m comment --comment "000 source destination ipv6 no cidr"', + :table => 'filter', + :params => { + :source => '2001:db8:85a3::8a2e:370:7334/128', + :destination => '2001:db8:85a3::8a2e:370:7334/128', + }, + }, + 'source_destination_ipv4_netmask' => { + :line => '-A INPUT -s 1.1.1.0/255.255.255.0 -d 2.2.0.0/255.255.0.0 -m comment --comment "000 source destination ipv4 netmask"', + :table => 'filter', + :params => { + :source => '1.1.1.0/24', + :destination => '2.2.0.0/16', + }, + }, + 'source_destination_ipv6_netmask' => { + :line => '-A INPUT -s 2001:db8:1234::/ffff:ffff:ffff:0000:0000:0000:0000:0000 -d 2001:db8:4321::/ffff:ffff:ffff:0000:0000:0000:0000:0000 -m comment --comment "000 source destination ipv6 netmask"', + :table => 'filter', + :params => { + :source => '2001:db8:1234::/48', + :destination => '2001:db8:4321::/48', + }, + }, + 'source_destination_negate_source' => { + :line => '-A INPUT ! -s 1.1.1.1 -d 2.2.2.2 -m comment --comment "000 negated source address"', + :table => 'filter', + :params => { + :source => '! 1.1.1.1/32', + :destination => '2.2.2.2/32', + }, + }, + 'source_destination_negate_destination' => { + :line => '-A INPUT -s 1.1.1.1 ! -d 2.2.2.2 -m comment --comment "000 negated destination address"', + :table => 'filter', + :params => { + :source => '1.1.1.1/32', + :destination => '! 2.2.2.2/32', + }, + }, + 'source_destination_negate_destination_alternative' => { + :line => '-A INPUT -s 1.1.1.1 -d ! 2.2.2.2 -m comment --comment "000 negated destination address alternative"', + :table => 'filter', + :params => { + :source => '1.1.1.1/32', + :destination => '! 2.2.2.2/32', + }, + }, + 'dport_range_1' => { + :line => '-A INPUT -m multiport --dports 1:1024 -m comment --comment "000 allow foo"', + :table => 'filter', + :params => { + :dport => ["1-1024"], + }, + }, + 'dport_range_2' => { + :line => '-A INPUT -m multiport --dports 15,512:1024 -m comment --comment "000 allow foo"', + :table => 'filter', + :params => { + :dport => ["15","512-1024"], + }, + }, + 'sport_range_1' => { + :line => '-A INPUT -m multiport --sports 1:1024 -m comment --comment "000 allow foo"', + :table => 'filter', + :params => { + :sport => ["1-1024"], + }, + }, + 'sport_range_2' => { + :line => '-A INPUT -m multiport --sports 15,512:1024 -m comment --comment "000 allow foo"', + :table => 'filter', + :params => { + :sport => ["15","512-1024"], + }, + }, + 'dst_type_1' => { + :line => '-A INPUT -m addrtype --dst-type LOCAL', + :table => 'filter', + :params => { + :dst_type => 'LOCAL', + }, + }, + 'src_type_1' => { + :line => '-A INPUT -m addrtype --src-type LOCAL', + :table => 'filter', + :params => { + :src_type => 'LOCAL', + }, + }, + 'dst_range_1' => { + :line => '-A INPUT -m iprange --dst-range 10.0.0.2-10.0.0.20', + :table => 'filter', + :params => { + :dst_range => '10.0.0.2-10.0.0.20', + }, + }, + 'src_range_1' => { + :line => '-A INPUT -m iprange --src-range 10.0.0.2-10.0.0.20', + :table => 'filter', + :params => { + :src_range => '10.0.0.2-10.0.0.20', + }, + }, + 'tcp_flags_1' => { + :line => '-A INPUT -p tcp -m tcp --tcp-flags SYN,RST,ACK,FIN SYN -m comment --comment "000 initiation"', + :table => 'filter', + :compare_all => true, + :chain => 'INPUT', + :proto => 'tcp', + :params => { + :chain => "INPUT", + :ensure => :present, + :line => '-A INPUT -p tcp -m tcp --tcp-flags SYN,RST,ACK,FIN SYN -m comment --comment "000 initiation"', + :name => "000 initiation", + :proto => "tcp", + :provider => "iptables", + :table => "filter", + :tcp_flags => "SYN,RST,ACK,FIN SYN", + }, + }, + 'state_returns_sorted_values' => { + :line => '-A INPUT -m state --state INVALID,RELATED,ESTABLISHED', + :table => 'filter', + :params => { + :state => ['ESTABLISHED', 'INVALID', 'RELATED'], + :action => nil, + }, + }, + 'ctstate_returns_sorted_values' => { + :line => '-A INPUT -m conntrack --ctstate INVALID,RELATED,ESTABLISHED', + :table => 'filter', + :params => { + :ctstate => ['ESTABLISHED', 'INVALID', 'RELATED'], + :action => nil, + }, + }, + 'comment_string_character_validation' => { + :line => '-A INPUT -s 192.168.0.1/32 -m comment --comment "000 allow from 192.168.0.1, please"', + :table => 'filter', + :params => { + :source => '192.168.0.1/32', + }, + }, + 'log_level_debug' => { + :line => '-A INPUT -m comment --comment "956 INPUT log-level" -m state --state NEW -j LOG --log-level 7', + :table => 'filter', + :params => { + :state => ['NEW'], + :log_level => '7', + :jump => 'LOG' + }, + }, + 'log_level_warn' => { + :line => '-A INPUT -m comment --comment "956 INPUT log-level" -m state --state NEW -j LOG', + :table => 'filter', + :params => { + :state => ['NEW'], + :log_level => '4', + :jump => 'LOG' + }, + }, + 'load_limit_module_and_implicit_burst' => { + :line => '-A INPUT -m multiport --dports 123 -m comment --comment "057 INPUT limit NTP" -m limit --limit 15/hour', + :table => 'filter', + :params => { + :dport => ['123'], + :limit => '15/hour', + :burst => '5' + }, + }, + 'limit_with_explicit_burst' => { + :line => '-A INPUT -m multiport --dports 123 -m comment --comment "057 INPUT limit NTP" -m limit --limit 30/hour --limit-burst 10', + :table => 'filter', + :params => { + :dport => ['123'], + :limit => '30/hour', + :burst => '10' + }, + }, + 'proto_ipencap' => { + :line => '-A INPUT -p ipencap -m comment --comment "0100 INPUT accept ipencap"', + :table => 'filter', + :params => { + :proto => 'ipencap', + } + }, + 'load_uid_owner_filter_module' => { + :line => '-A OUTPUT -m owner --uid-owner root -m comment --comment "057 OUTPUT uid root only" -j ACCEPT', + :table => 'filter', + :params => { + :action => 'accept', + :uid => 'root', + :chain => 'OUTPUT', + }, + }, + 'load_uid_owner_postrouting_module' => { + :line => '-t mangle -A POSTROUTING -m owner --uid-owner root -m comment --comment "057 POSTROUTING uid root only" -j ACCEPT', + :table => 'mangle', + :params => { + :action => 'accept', + :chain => 'POSTROUTING', + :uid => 'root', + }, + }, + 'load_gid_owner_filter_module' => { + :line => '-A OUTPUT -m owner --gid-owner root -m comment --comment "057 OUTPUT gid root only" -j ACCEPT', + :table => 'filter', + :params => { + :action => 'accept', + :chain => 'OUTPUT', + :gid => 'root', + }, + }, + 'load_gid_owner_postrouting_module' => { + :line => '-t mangle -A POSTROUTING -m owner --gid-owner root -m comment --comment "057 POSTROUTING gid root only" -j ACCEPT', + :table => 'mangle', + :params => { + :action => 'accept', + :chain => 'POSTROUTING', + :gid => 'root', + }, + }, + 'mark_set-mark' => { + :line => '-t mangle -A PREROUTING -j MARK --set-xmark 0x3e8/0xffffffff', + :table => 'mangle', + :params => { + :jump => 'MARK', + :chain => 'PREROUTING', + :set_mark => '0x3e8/0xffffffff', + } + }, + 'iniface_1' => { + :line => '-A INPUT -i eth0 -m comment --comment "060 iniface" -j DROP', + :table => 'filter', + :params => { + :action => 'drop', + :chain => 'INPUT', + :iniface => 'eth0', + }, + }, + 'iniface_with_vlans_1' => { + :line => '-A INPUT -i eth0.234 -m comment --comment "060 iniface" -j DROP', + :table => 'filter', + :params => { + :action => 'drop', + :chain => 'INPUT', + :iniface => 'eth0.234', + }, + }, + 'iniface_with_plus_1' => { + :line => '-A INPUT -i eth+ -m comment --comment "060 iniface" -j DROP', + :table => 'filter', + :params => { + :action => 'drop', + :chain => 'INPUT', + :iniface => 'eth+', + }, + }, + 'outiface_1' => { + :line => '-A OUTPUT -o eth0 -m comment --comment "060 outiface" -j DROP', + :table => 'filter', + :params => { + :action => 'drop', + :chain => 'OUTPUT', + :outiface => 'eth0', + }, + }, + 'outiface_with_vlans_1' => { + :line => '-A OUTPUT -o eth0.234 -m comment --comment "060 outiface" -j DROP', + :table => 'filter', + :params => { + :action => 'drop', + :chain => 'OUTPUT', + :outiface => 'eth0.234', + }, + }, + 'outiface_with_plus_1' => { + :line => '-A OUTPUT -o eth+ -m comment --comment "060 outiface" -j DROP', + :table => 'filter', + :params => { + :action => 'drop', + :chain => 'OUTPUT', + :outiface => 'eth+', + }, + }, + 'pkttype multicast' => { + :line => '-A INPUT -m pkttype --pkt-type multicast -j ACCEPT', + :table => 'filter', + :params => { + :action => 'accept', + :pkttype => 'multicast', + }, + }, + 'socket_option' => { + :line => '-A PREROUTING -m socket -j ACCEPT', + :table => 'mangle', + :params => { + :action => 'accept', + :chain => 'PREROUTING', + :socket => true, + }, + }, + 'isfragment_option' => { + :line => '-A INPUT -f -m comment --comment "010 a-f comment with dashf" -j ACCEPT', + :table => 'filter', + :params => { + :name => '010 a-f comment with dashf', + :action => 'accept', + :isfragment => true, + }, + }, + 'single_tcp_sport' => { + :line => '-A OUTPUT -s 10.94.100.46/32 -p tcp -m tcp --sport 20443 -j ACCEPT', + :table => 'mangle', + :params => { + :action => 'accept', + :chain => 'OUTPUT', + :source => "10.94.100.46/32", + :proto => "tcp", + :sport => ["20443"], + }, + }, + 'single_udp_sport' => { + :line => '-A OUTPUT -s 10.94.100.46/32 -p udp -m udp --sport 20443 -j ACCEPT', + :table => 'mangle', + :params => { + :action => 'accept', + :chain => 'OUTPUT', + :source => "10.94.100.46/32", + :proto => "udp", + :sport => ["20443"], + }, + }, + 'single_tcp_dport' => { + :line => '-A OUTPUT -s 10.94.100.46/32 -p tcp -m tcp --dport 20443 -j ACCEPT', + :table => 'mangle', + :params => { + :action => 'accept', + :chain => 'OUTPUT', + :source => "10.94.100.46/32", + :proto => "tcp", + :dport => ["20443"], + }, + }, + 'single_udp_dport' => { + :line => '-A OUTPUT -s 10.94.100.46/32 -p udp -m udp --dport 20443 -j ACCEPT', + :table => 'mangle', + :params => { + :action => 'accept', + :chain => 'OUTPUT', + :source => "10.94.100.46/32", + :proto => "udp", + :dport => ["20443"], + }, + }, + 'connlimit_above' => { + :line => '-A INPUT -p tcp -m multiport --dports 22 -m comment --comment "061 REJECT connlimit_above 10" -m connlimit --connlimit-above 10 --connlimit-mask 32 -j REJECT --reject-with icmp-port-unreachable', + :table => 'filter', + :params => { + :proto => 'tcp', + :dport => ["22"], + :connlimit_above => '10', + :action => 'reject', + }, + }, + 'connlimit_above_with_connlimit_mask' => { + :line => '-A INPUT -p tcp -m multiport --dports 22 -m comment --comment "061 REJECT connlimit_above 10 with mask 24" -m connlimit --connlimit-above 10 --connlimit-mask 24 -j REJECT --reject-with icmp-port-unreachable', + :table => 'filter', + :params => { + :proto => 'tcp', + :dport => ["22"], + :connlimit_above => '10', + :connlimit_mask => '24', + :action => 'reject', + }, + }, + 'connmark' => { + :line => '-A INPUT -m comment --comment "062 REJECT connmark" -m connmark --mark 0x1 -j REJECT --reject-with icmp-port-unreachable', + :table => 'filter', + :params => { + :proto => 'all', + :connmark => '0x1', + :action => 'reject', + }, + }, + 'disallow_esp_protocol' => { + :line => '-t filter ! -p esp -m comment --comment "063 disallow esp protocol" -j ACCEPT', + :table => 'filter', + :params => { + :name => '063 disallow esp protocol', + :action => 'accept', + :proto => '! esp', + }, + }, + 'drop_new_packets_without_syn' => { + :line => '-t filter ! -s 10.0.0.0/8 ! -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m comment --comment "064 drop NEW non-tcp external packets with FIN/RST/ACK set and SYN unset" -m state --state NEW -j DROP', + :table => 'filter', + :params => { + :name => '064 drop NEW non-tcp external packets with FIN/RST/ACK set and SYN unset', + :state => ['NEW'], + :action => 'drop', + :proto => '! tcp', + :source => '! 10.0.0.0/8', + :tcp_flags => '! FIN,SYN,RST,ACK SYN', + }, + }, + 'negate_dport_and_sport' => { + :line => '-A nova-compute-FORWARD -s 0.0.0.0/32 -d 255.255.255.255/32 -p udp -m udp ! --sport 68,69 ! --dport 67,66 -j ACCEPT', + :table => 'filter', + :params => { + :action => 'accept', + :chain => 'nova-compute-FORWARD', + :source => '0.0.0.0/32', + :destination => '255.255.255.255/32', + :sport => ['! 68','! 69'], + :dport => ['! 67','! 66'], + :proto => 'udp', + }, + }, +} + +# This hash is for testing converting a hash to an argument line. +HASH_TO_ARGS = { + 'long_rule_1' => { + :params => { + :action => "accept", + :chain => "INPUT", + :destination => "1.1.1.1", + :dport => ["7061","7062"], + :ensure => :present, + :name => "000 allow foo", + :proto => "tcp", + :source => "1.1.1.1", + :sport => ["7061","7062"], + :table => "filter", + }, + :args => ["-t", :filter, "-s", "1.1.1.1/32", "-d", "1.1.1.1/32", "-p", :tcp, "-m", "multiport", "--sports", "7061,7062", "-m", "multiport", "--dports", "7061,7062", "-m", "comment", "--comment", "000 allow foo", "-j", "ACCEPT"], + }, + 'long_rule_2' => { + :params => { + :chain => "INPUT", + :destination => "2.10.13.3/24", + :dport => ["7061"], + :ensure => :present, + :jump => "my_custom_chain", + :name => "700 allow bar", + :proto => "udp", + :source => "1.1.1.1", + :sport => ["7061","7062"], + :table => "filter", + }, + :args => ["-t", :filter, "-s", "1.1.1.1/32", "-d", "2.10.13.0/24", "-p", :udp, "-m", "multiport", "--sports", "7061,7062", "-m", "multiport", "--dports", "7061", "-m", "comment", "--comment", "700 allow bar", "-j", "my_custom_chain"], + }, + 'no_action' => { + :params => { + :name => "100 no action", + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment", + "100 no action"], + }, + 'zero_prefixlen_ipv4' => { + :params => { + :name => '100 zero prefix length ipv4', + :table => 'filter', + :source => '0.0.0.0/0', + :destination => '0.0.0.0/0', + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'comment', '--comment', '100 zero prefix length ipv4'], + }, + 'zero_prefixlen_ipv6' => { + :params => { + :name => '100 zero prefix length ipv6', + :table => 'filter', + :source => '::/0', + :destination => '::/0', + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'comment', '--comment', '100 zero prefix length ipv6'], + }, + 'source_destination_ipv4_no_cidr' => { + :params => { + :name => '000 source destination ipv4 no cidr', + :table => 'filter', + :source => '1.1.1.1', + :destination => '2.2.2.2', + }, + :args => ['-t', :filter, '-s', '1.1.1.1/32', '-d', '2.2.2.2/32', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv4 no cidr'], + }, + 'source_destination_ipv6_no_cidr' => { + :params => { + :name => '000 source destination ipv6 no cidr', + :table => 'filter', + :source => '2001:db8:1234::', + :destination => '2001:db8:4321::', + }, + :args => ['-t', :filter, '-s', '2001:db8:1234::/128', '-d', '2001:db8:4321::/128', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv6 no cidr'], + }, + 'source_destination_ipv4_netmask' => { + :params => { + :name => '000 source destination ipv4 netmask', + :table => 'filter', + :source => '1.1.1.0/255.255.255.0', + :destination => '2.2.0.0/255.255.0.0', + }, + :args => ['-t', :filter, '-s', '1.1.1.0/24', '-d', '2.2.0.0/16', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv4 netmask'], + }, + 'source_destination_ipv6_netmask' => { + :params => { + :name => '000 source destination ipv6 netmask', + :table => 'filter', + :source => '2001:db8:1234::/ffff:ffff:ffff:0000:0000:0000:0000:0000', + :destination => '2001:db8:4321::/ffff:ffff:ffff:0000:0000:0000:0000:0000', + }, + :args => ['-t', :filter, '-s', '2001:db8:1234::/48', '-d', '2001:db8:4321::/48', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv6 netmask'], + }, + 'sport_range_1' => { + :params => { + :name => "100 sport range", + :sport => ["1-1024"], + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "multiport", "--sports", "1:1024", "-m", "comment", "--comment", "100 sport range"], + }, + 'sport_range_2' => { + :params => { + :name => "100 sport range", + :sport => ["15","512-1024"], + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "multiport", "--sports", "15,512:1024", "-m", "comment", "--comment", "100 sport range"], + }, + 'dport_range_1' => { + :params => { + :name => "100 sport range", + :dport => ["1-1024"], + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "multiport", "--dports", "1:1024", "-m", "comment", "--comment", "100 sport range"], + }, + 'dport_range_2' => { + :params => { + :name => "100 sport range", + :dport => ["15","512-1024"], + :table => "filter", + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "multiport", "--dports", "15,512:1024", "-m", "comment", "--comment", "100 sport range"], + }, + 'dst_type_1' => { + :params => { + :name => '000 dst_type', + :table => 'filter', + :dst_type => 'LOCAL', + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--dst-type', :LOCAL, '-m', 'comment', '--comment', '000 dst_type'], + }, + 'src_type_1' => { + :params => { + :name => '000 src_type', + :table => 'filter', + :src_type => 'LOCAL', + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--src-type', :LOCAL, '-m', 'comment', '--comment', '000 src_type'], + }, + 'dst_range_1' => { + :params => { + :name => '000 dst_range', + :table => 'filter', + :dst_range => '10.0.0.1-10.0.0.10', + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'iprange', '--dst-range', '10.0.0.1-10.0.0.10', '-m', 'comment', '--comment', '000 dst_range'], + }, + 'src_range_1' => { + :params => { + :name => '000 src_range', + :table => 'filter', + :dst_range => '10.0.0.1-10.0.0.10', + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'iprange', '--dst-range', '10.0.0.1-10.0.0.10', '-m', 'comment', '--comment', '000 src_range'], + }, + 'tcp_flags_1' => { + :params => { + :name => "000 initiation", + :tcp_flags => "SYN,RST,ACK,FIN SYN", + :table => "filter", + }, + + :args => ["-t", :filter, "-p", :tcp, "-m", "tcp", "--tcp-flags", "SYN,RST,ACK,FIN", "SYN", "-m", "comment", "--comment", "000 initiation",] + }, + 'states_set_from_array' => { + :params => { + :name => "100 states_set_from_array", + :table => "filter", + :state => ['ESTABLISHED', 'INVALID'] + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment", "100 states_set_from_array", + "-m", "state", "--state", "ESTABLISHED,INVALID"], + }, + 'ctstates_set_from_array' => { + :params => { + :name => "100 ctstates_set_from_array", + :table => "filter", + :ctstate => ['ESTABLISHED', 'INVALID'] + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment", "100 ctstates_set_from_array", + "-m", "conntrack", "--ctstate", "ESTABLISHED,INVALID"], + }, + 'comment_string_character_validation' => { + :params => { + :name => "000 allow from 192.168.0.1, please", + :table => 'filter', + :source => '192.168.0.1' + }, + :args => ['-t', :filter, '-s', '192.168.0.1/32', '-p', :tcp, '-m', 'comment', '--comment', '000 allow from 192.168.0.1, please'], + }, + 'port_property' => { + :params => { + :name => '001 port property', + :table => 'filter', + :port => '80', + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'multiport', '--ports', '80', '-m', 'comment', '--comment', '001 port property'], + }, + 'log_level_debug' => { + :params => { + :name => '956 INPUT log-level', + :table => 'filter', + :state => 'NEW', + :jump => 'LOG', + :log_level => 'debug' + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'comment', '--comment', '956 INPUT log-level', '-m', 'state', '--state', 'NEW', '-j', 'LOG', '--log-level', '7'], + }, + 'log_level_warn' => { + :params => { + :name => '956 INPUT log-level', + :table => 'filter', + :state => 'NEW', + :jump => 'LOG', + :log_level => 'warn' + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'comment', '--comment', '956 INPUT log-level', '-m', 'state', '--state', 'NEW', '-j', 'LOG', '--log-level', '4'], + }, + 'load_limit_module_and_implicit_burst' => { + :params => { + :name => '057 INPUT limit NTP', + :table => 'filter', + :dport => '123', + :limit => '15/hour' + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'multiport', '--dports', '123', '-m', 'comment', '--comment', '057 INPUT limit NTP', '-m', 'limit', '--limit', '15/hour'], + }, + 'limit_with_explicit_burst' => { + :params => { + :name => '057 INPUT limit NTP', + :table => 'filter', + :dport => '123', + :limit => '30/hour', + :burst => '10' + }, + :args => ['-t', :filter, '-p', :tcp, '-m', 'multiport', '--dports', '123', '-m', 'comment', '--comment', '057 INPUT limit NTP', '-m', 'limit', '--limit', '30/hour', '--limit-burst', '10'], + }, + 'proto_ipencap' => { + :params => { + :name => '0100 INPUT accept ipencap', + :table => 'filter', + :proto => 'ipencap', + }, + :args => ['-t', :filter, '-p', :ipencap, '-m', 'comment', '--comment', '0100 INPUT accept ipencap'], + }, + 'load_uid_owner_filter_module' => { + :params => { + :name => '057 OUTPUT uid root only', + :table => 'filter', + :uid => 'root', + :action => 'accept', + :chain => 'OUTPUT', + :proto => 'all', + }, + :args => ['-t', :filter, '-p', :all, '-m', 'owner', '--uid-owner', 'root', '-m', 'comment', '--comment', '057 OUTPUT uid root only', '-j', 'ACCEPT'], + }, + 'load_uid_owner_postrouting_module' => { + :params => { + :name => '057 POSTROUTING uid root only', + :table => 'mangle', + :uid => 'root', + :action => 'accept', + :chain => 'POSTROUTING', + :proto => 'all', + }, + :args => ['-t', :mangle, '-p', :all, '-m', 'owner', '--uid-owner', 'root', '-m', 'comment', '--comment', '057 POSTROUTING uid root only', '-j', 'ACCEPT'], + }, + 'load_gid_owner_filter_module' => { + :params => { + :name => '057 OUTPUT gid root only', + :table => 'filter', + :chain => 'OUTPUT', + :gid => 'root', + :action => 'accept', + :proto => 'all', + }, + :args => ['-t', :filter, '-p', :all, '-m', 'owner', '--gid-owner', 'root', '-m', 'comment', '--comment', '057 OUTPUT gid root only', '-j', 'ACCEPT'], + }, + 'load_gid_owner_postrouting_module' => { + :params => { + :name => '057 POSTROUTING gid root only', + :table => 'mangle', + :gid => 'root', + :action => 'accept', + :chain => 'POSTROUTING', + :proto => 'all', + }, + :args => ['-t', :mangle, '-p', :all, '-m', 'owner', '--gid-owner', 'root', '-m', 'comment', '--comment', '057 POSTROUTING gid root only', '-j', 'ACCEPT'], + }, + 'mark_set-mark_int' => { + :params => { + :name => '058 set-mark 1000', + :table => 'mangle', + :jump => 'MARK', + :chain => 'PREROUTING', + :set_mark => '1000', + }, + :args => ['-t', :mangle, '-p', :tcp, '-m', 'comment', '--comment', '058 set-mark 1000', '-j', 'MARK', '--set-xmark', '0x3e8/0xffffffff'], + }, + 'mark_set-mark_hex' => { + :params => { + :name => '058 set-mark 0x32', + :table => 'mangle', + :jump => 'MARK', + :chain => 'PREROUTING', + :set_mark => '0x32', + }, + :args => ['-t', :mangle, '-p', :tcp, '-m', 'comment', '--comment', '058 set-mark 0x32', '-j', 'MARK', '--set-xmark', '0x32/0xffffffff'], + }, + 'mark_set-mark_hex_with_hex_mask' => { + :params => { + :name => '058 set-mark 0x32/0xffffffff', + :table => 'mangle', + :jump => 'MARK', + :chain => 'PREROUTING', + :set_mark => '0x32/0xffffffff', + }, + :args => ['-t', :mangle, '-p', :tcp, '-m', 'comment', '--comment', '058 set-mark 0x32/0xffffffff', '-j', 'MARK', '--set-xmark', '0x32/0xffffffff'], + }, + 'mark_set-mark_hex_with_mask' => { + :params => { + :name => '058 set-mark 0x32/4', + :table => 'mangle', + :jump => 'MARK', + :chain => 'PREROUTING', + :set_mark => '0x32/4', + }, + :args => ['-t', :mangle, '-p', :tcp, '-m', 'comment', '--comment', '058 set-mark 0x32/4', '-j', 'MARK', '--set-xmark', '0x32/0x4'], + }, + 'iniface_1' => { + :params => { + :name => '060 iniface', + :table => 'filter', + :action => 'drop', + :chain => 'INPUT', + :iniface => 'eth0', + }, + :args => ["-t", :filter, "-i", "eth0", "-p", :tcp, "-m", "comment", "--comment", "060 iniface", "-j", "DROP"], + }, + 'iniface_with_vlans_1' => { + :params => { + :name => '060 iniface', + :table => 'filter', + :action => 'drop', + :chain => 'INPUT', + :iniface => 'eth0.234', + }, + :args => ["-t", :filter, "-i", "eth0.234", "-p", :tcp, "-m", "comment", "--comment", "060 iniface", "-j", "DROP"], + }, + 'iniface_with_plus_1' => { + :params => { + :name => '060 iniface', + :table => 'filter', + :action => 'drop', + :chain => 'INPUT', + :iniface => 'eth+', + }, + :args => ["-t", :filter, "-i", "eth+", "-p", :tcp, "-m", "comment", "--comment", "060 iniface", "-j", "DROP"], + }, + 'outiface_1' => { + :params => { + :name => '060 outiface', + :table => 'filter', + :action => 'drop', + :chain => 'OUTPUT', + :outiface => 'eth0', + }, + :args => ["-t", :filter, "-o", "eth0", "-p", :tcp, "-m", "comment", "--comment", "060 outiface", "-j", "DROP"], + }, + 'outiface_with_vlans_1' => { + :params => { + :name => '060 outiface', + :table => 'filter', + :action => 'drop', + :chain => 'OUTPUT', + :outiface => 'eth0.234', + }, + :args => ["-t", :filter, "-o", "eth0.234", "-p", :tcp, "-m", "comment", "--comment", "060 outiface", "-j", "DROP"], + }, + 'outiface_with_plus_1' => { + :params => { + :name => '060 outiface', + :table => 'filter', + :action => 'drop', + :chain => 'OUTPUT', + :outiface => 'eth+', + }, + :args => ["-t", :filter, "-o", "eth+", "-p", :tcp, "-m", "comment", "--comment", "060 outiface", "-j", "DROP"], + }, + 'pkttype multicast' => { + :params => { + :name => '062 pkttype multicast', + :table => "filter", + :action => 'accept', + :chain => 'INPUT', + :iniface => 'eth0', + :pkttype => 'multicast', + }, + :args => ["-t", :filter, "-i", "eth0", "-p", :tcp, "-m", "pkttype", "--pkt-type", :multicast, "-m", "comment", "--comment", "062 pkttype multicast", "-j", "ACCEPT"], + }, + 'socket_option' => { + :params => { + :name => '050 socket option', + :table => 'mangle', + :action => 'accept', + :chain => 'PREROUTING', + :socket => true, + }, + :args => ['-t', :mangle, '-p', :tcp, '-m', 'socket', '-m', 'comment', '--comment', '050 socket option', '-j', 'ACCEPT'], + }, + 'isfragment_option' => { + :params => { + :name => '050 isfragment option', + :table => 'filter', + :proto => :all, + :action => 'accept', + :isfragment => true, + }, + :args => ['-t', :filter, '-p', :all, '-f', '-m', 'comment', '--comment', '050 isfragment option', '-j', 'ACCEPT'], + }, + 'isfragment_option not changing -f in comment' => { + :params => { + :name => '050 testcomment-with-fdashf', + :table => 'filter', + :proto => :all, + :action => 'accept', + }, + :args => ['-t', :filter, '-p', :all, '-m', 'comment', '--comment', '050 testcomment-with-fdashf', '-j', 'ACCEPT'], + }, + 'connlimit_above' => { + :params => { + :name => '061 REJECT connlimit_above 10', + :table => 'filter', + :proto => 'tcp', + :dport => ["22"], + :connlimit_above => '10', + :action => 'reject', + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "multiport", "--dports", "22", "-m", "comment", "--comment", "061 REJECT connlimit_above 10", "-j", "REJECT", "-m", "connlimit", "--connlimit-above", "10"], + }, + 'connlimit_above_with_connlimit_mask' => { + :params => { + :name => '061 REJECT connlimit_above 10 with mask 24', + :table => 'filter', + :proto => 'tcp', + :dport => ["22"], + :connlimit_above => '10', + :connlimit_mask => '24', + :action => 'reject', + }, + :args => ["-t", :filter, "-p", :tcp, "-m", "multiport", "--dports", "22", "-m", "comment", "--comment", "061 REJECT connlimit_above 10 with mask 24", "-j", "REJECT", "-m", "connlimit", "--connlimit-above", "10", "--connlimit-mask", "24"], + }, + 'connmark' => { + :params => { + :name => '062 REJECT connmark', + :table => 'filter', + :proto => 'all', + :connmark => '0x1', + :action => 'reject', + }, + :args => ["-t", :filter, "-p", :all, "-m", "comment", "--comment", "062 REJECT connmark", "-j", "REJECT", "-m", "connmark", "--mark", "0x1"], + }, + 'disallow_esp_protocol' => { + :params => { + :name => '063 disallow esp protocol', + :table => 'filter', + :action => 'accept', + :proto => '! esp', + }, + :args => ["-t", :filter, "!", "-p", :esp, "-m", "comment", "--comment", "063 disallow esp protocol", "-j", "ACCEPT"], + }, + 'drop_new_packets_without_syn' => { + :params => { + :name => '064 drop NEW non-tcp external packets with FIN/RST/ACK set and SYN unset', + :table => 'filter', + :chain => 'INPUT', + :state => ['NEW'], + :action => 'drop', + :proto => '! tcp', + :source => '! 10.0.0.0/8', + :tcp_flags => '! FIN,SYN,RST,ACK SYN', + }, + :args => ["-t", :filter, "!", "-s", "10.0.0.0/8", "!", "-p", :tcp, "-m", "tcp", "!", "--tcp-flags", "FIN,SYN,RST,ACK", "SYN", "-m", "comment", "--comment", "064 drop NEW non-tcp external packets with FIN/RST/ACK set and SYN unset", "-m", "state", "--state", "NEW", "-j", "DROP"] + }, + 'negate_dport_and_sport' => { + :params => { + :name => '065 negate dport and sport', + :table => 'filter', + :action => 'accept', + :chain => 'nova-compute-FORWARD', + :source => '0.0.0.0/32', + :destination => '255.255.255.255/32', + :sport => ['! 68','! 69'], + :dport => ['! 67','! 66'], + :proto => 'udp', + }, + :args => ["-t", :filter, "-s", "0.0.0.0/32", "-d", "255.255.255.255/32", "-p", :udp, "-m", "multiport", "!", "--sports", "68,69", "-m", "multiport", "!", "--dports", "67,66", "-m", "comment", "--comment", "065 negate dport and sport", "-j", "ACCEPT"], + }, +} diff --git a/firewall/spec/spec.opts b/firewall/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/firewall/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/firewall/spec/spec_helper.rb b/firewall/spec/spec_helper.rb new file mode 100644 index 000000000..dc8bc39cb --- /dev/null +++ b/firewall/spec/spec_helper.rb @@ -0,0 +1,29 @@ +dir = File.expand_path(File.dirname(__FILE__)) +$LOAD_PATH.unshift File.join(dir, 'lib') + +# Don't want puppet getting the command line arguments for rake or autotest +ARGV.clear + +require 'rubygems' +require 'bundler/setup' +require 'rspec-puppet' + +Bundler.require :default, :test + +require 'pathname' +require 'tmpdir' + +Pathname.glob("#{dir}/shared_behaviours/**/*.rb") do |behaviour| + require behaviour.relative_path_from(Pathname.new(dir)) +end + +fixture_path = File.expand_path(File.join(__FILE__, '..', 'fixtures')) + +RSpec.configure do |config| + config.tty = true + config.mock_with :rspec do |c| + c.syntax = :expect + end + config.module_path = File.join(fixture_path, 'modules') + config.manifest_dir = File.join(fixture_path, 'manifests') +end diff --git a/firewall/spec/spec_helper_acceptance.rb b/firewall/spec/spec_helper_acceptance.rb new file mode 100644 index 000000000..ca29ce1cb --- /dev/null +++ b/firewall/spec/spec_helper_acceptance.rb @@ -0,0 +1,45 @@ +require 'beaker-rspec' + +def iptables_flush_all_tables + ['filter', 'nat', 'mangle', 'raw'].each do |t| + expect(shell("iptables -t #{t} -F").stderr).to eq("") + end +end + +def ip6tables_flush_all_tables + ['filter'].each do |t| + expect(shell("ip6tables -t #{t} -F").stderr).to eq("") + end +end + +unless ENV['RS_PROVISION'] == 'no' or ENV['BEAKER_provision'] == 'no' + # This will install the latest available package on el and deb based + # systems fail on windows and osx, and install via gem on other *nixes + foss_opts = { :default_action => 'gem_install' } + + if default.is_pe?; then install_pe; else install_puppet( foss_opts ); end + + hosts.each do |host| + on host, "mkdir -p #{host['distmoduledir']}" + end +end + +UNSUPPORTED_PLATFORMS = ['windows','Solaris','Darwin'] + +RSpec.configure do |c| + # Project root + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Readable test descriptions + c.formatter = :documentation + + # Configure all nodes in nodeset + c.before :suite do + # Install module and dependencies + puppet_module_install(:source => proj_root, :module_name => 'firewall') + hosts.each do |host| + shell('/bin/touch /etc/puppet/hiera.yaml') + shell('puppet module install puppetlabs-stdlib --version 3.2.0', { :acceptable_exit_codes => [0,1] }) + end + end +end diff --git a/firewall/spec/unit/classes/firewall_linux_archlinux_spec.rb b/firewall/spec/unit/classes/firewall_linux_archlinux_spec.rb new file mode 100644 index 000000000..954d9ee10 --- /dev/null +++ b/firewall/spec/unit/classes/firewall_linux_archlinux_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'firewall::linux::archlinux', :type => :class do + it { should contain_service('iptables').with( + :ensure => 'running', + :enable => 'true' + )} + it { should contain_service('ip6tables').with( + :ensure => 'running', + :enable => 'true' + )} + + context 'ensure => stopped' do + let(:params) {{ :ensure => 'stopped' }} + it { should contain_service('iptables').with( + :ensure => 'stopped' + )} + it { should contain_service('ip6tables').with( + :ensure => 'stopped' + )} + end + + context 'enable => false' do + let(:params) {{ :enable => 'false' }} + it { should contain_service('iptables').with( + :enable => 'false' + )} + it { should contain_service('ip6tables').with( + :enable => 'false' + )} + end +end diff --git a/firewall/spec/unit/classes/firewall_linux_debian_spec.rb b/firewall/spec/unit/classes/firewall_linux_debian_spec.rb new file mode 100644 index 000000000..98285b642 --- /dev/null +++ b/firewall/spec/unit/classes/firewall_linux_debian_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe 'firewall::linux::debian', :type => :class do + it { should contain_package('iptables-persistent').with( + :ensure => 'present' + )} + it { should contain_service('iptables-persistent').with( + :ensure => nil, + :enable => 'true', + :require => 'Package[iptables-persistent]' + )} + + context 'enable => false' do + let(:params) {{ :enable => 'false' }} + it { should contain_service('iptables-persistent').with( + :enable => 'false' + )} + end +end diff --git a/firewall/spec/unit/classes/firewall_linux_redhat_spec.rb b/firewall/spec/unit/classes/firewall_linux_redhat_spec.rb new file mode 100644 index 000000000..43263fa67 --- /dev/null +++ b/firewall/spec/unit/classes/firewall_linux_redhat_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe 'firewall::linux::redhat', :type => :class do + %w{RedHat CentOS Fedora}.each do |os| + oldreleases = (os == 'Fedora' ? ['14'] : ['6.5']) + newreleases = (os == 'Fedora' ? ['15','Rawhide'] : ['7.0.1406']) + + oldreleases.each do |osrel| + context "os #{os} and osrel #{osrel}" do + let(:facts) {{ + :operatingsystem => os, + :operatingsystemrelease => osrel + }} + + it { should_not contain_package('firewalld') } + it { should_not contain_package('iptables-services') } + end + end + + newreleases.each do |osrel| + context "os #{os} and osrel #{osrel}" do + let(:facts) {{ + :operatingsystem => os, + :operatingsystemrelease => osrel + }} + + it { should contain_package('firewalld').with( + :ensure => 'absent', + :before => 'Package[iptables-services]' + )} + + it { should contain_package('iptables-services').with( + :ensure => 'present', + :before => 'Service[iptables]' + )} + end + end + + describe 'ensure' do + context 'default' do + it { should contain_service('iptables').with( + :ensure => 'running', + :enable => 'true' + )} + end + context 'ensure => stopped' do + let(:params) {{ :ensure => 'stopped' }} + it { should contain_service('iptables').with( + :ensure => 'stopped' + )} + end + context 'enable => false' do + let(:params) {{ :enable => 'false' }} + it { should contain_service('iptables').with( + :enable => 'false' + )} + end + end + end +end diff --git a/firewall/spec/unit/classes/firewall_linux_spec.rb b/firewall/spec/unit/classes/firewall_linux_spec.rb new file mode 100644 index 000000000..e43c1e9f8 --- /dev/null +++ b/firewall/spec/unit/classes/firewall_linux_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe 'firewall::linux', :type => :class do + let(:facts_default) {{ :kernel => 'Linux' }} + it { should contain_package('iptables').with_ensure('present') } + + context 'RedHat like' do + %w{RedHat CentOS Fedora}.each do |os| + context "operatingsystem => #{os}" do + releases = (os == 'Fedora' ? ['14','15','Rawhide'] : ['6','7']) + releases.each do |osrel| + context "operatingsystemrelease => #{osrel}" do + let(:facts) { facts_default.merge({ :operatingsystem => os, + :operatingsystemrelease => osrel}) } + it { should contain_class('firewall::linux::redhat').with_require('Package[iptables]') } + end + end + end + end + end + + context 'Debian like' do + %w{Debian Ubuntu}.each do |os| + context "operatingsystem => #{os}" do + let(:facts) { facts_default.merge({ :operatingsystem => os }) } + it { should contain_class('firewall::linux::debian').with_require('Package[iptables]') } + end + end + end +end diff --git a/firewall/spec/unit/classes/firewall_spec.rb b/firewall/spec/unit/classes/firewall_spec.rb new file mode 100644 index 000000000..cbfb48c39 --- /dev/null +++ b/firewall/spec/unit/classes/firewall_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'firewall', :type => :class do + context 'kernel => Linux' do + let(:facts) {{ :kernel => 'Linux' }} + it { should contain_class('firewall::linux').with_ensure('running') } + end + + context 'kernel => Windows' do + let(:facts) {{ :kernel => 'Windows' }} + it { expect { should contain_class('firewall::linux') }.to raise_error(Puppet::Error) } + end + + context 'kernel => SunOS' do + let(:facts) {{ :kernel => 'SunOS' }} + it { expect { should contain_class('firewall::linux') }.to raise_error(Puppet::Error) } + end + + context 'kernel => Darwin' do + let(:facts) {{ :kernel => 'Darwin' }} + it { expect { should contain_class('firewall::linux') }.to raise_error(Puppet::Error) } + end + + context 'ensure => stopped' do + let(:facts) {{ :kernel => 'Linux' }} + let(:params) {{ :ensure => 'stopped' }} + it { should contain_class('firewall::linux').with_ensure('stopped') } + end + + context 'ensure => test' do + let(:facts) {{ :kernel => 'Linux' }} + let(:params) {{ :ensure => 'test' }} + it { expect { should contain_class('firewall::linux') }.to raise_error(Puppet::Error) } + end +end diff --git a/firewall/spec/unit/facter/iptables_persistent_version_spec.rb b/firewall/spec/unit/facter/iptables_persistent_version_spec.rb new file mode 100644 index 000000000..13a23a5c2 --- /dev/null +++ b/firewall/spec/unit/facter/iptables_persistent_version_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe "Facter::Util::Fact iptables_persistent_version" do + before { Facter.clear } + let(:dpkg_cmd) { "dpkg-query -Wf '${Version}' iptables-persistent 2>/dev/null" } + + { + "Debian" => "0.0.20090701", + "Ubuntu" => "0.5.3ubuntu2", + }.each do |os, ver| + describe "#{os} package installed" do + before { + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return(os) + allow(Facter::Util::Resolution).to receive(:exec).with(dpkg_cmd). + and_return(ver) + } + it { Facter.fact(:iptables_persistent_version).value.should == ver } + end + end + + describe 'Ubuntu package not installed' do + before { + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Ubuntu') + allow(Facter::Util::Resolution).to receive(:exec).with(dpkg_cmd). + and_return(nil) + } + it { Facter.fact(:iptables_persistent_version).value.should be_nil } + end + + describe 'CentOS not supported' do + before { allow(Facter.fact(:operatingsystem)).to receive(:value). + and_return("CentOS") } + it { Facter.fact(:iptables_persistent_version).value.should be_nil } + end +end diff --git a/firewall/spec/unit/facter/iptables_spec.rb b/firewall/spec/unit/facter/iptables_spec.rb new file mode 100644 index 000000000..5773fdce5 --- /dev/null +++ b/firewall/spec/unit/facter/iptables_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe "Facter::Util::Fact" do + before { + Facter.clear + allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') + allow(Facter.fact(:kernelrelease)).to receive(:value).and_return('2.6') + } + + describe 'iptables_version' do + it { + allow(Facter::Util::Resolution).to receive(:exec).with('iptables --version'). + and_return('iptables v1.4.7') + Facter.fact(:iptables_version).value.should == '1.4.7' + } + end + + describe 'ip6tables_version' do + before { allow(Facter::Util::Resolution).to receive(:exec). + with('ip6tables --version').and_return('ip6tables v1.4.7') } + it { Facter.fact(:ip6tables_version).value.should == '1.4.7' } + end +end diff --git a/firewall/spec/unit/puppet/provider/iptables_chain_spec.rb b/firewall/spec/unit/puppet/provider/iptables_chain_spec.rb new file mode 100755 index 000000000..e2c0fd3d3 --- /dev/null +++ b/firewall/spec/unit/puppet/provider/iptables_chain_spec.rb @@ -0,0 +1,231 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +if Puppet.version < '3.4.0' + require 'puppet/provider/confine/exists' +else + require 'puppet/confine/exists' +end + +describe 'iptables chain provider detection' do + if Puppet.version < '3.4.0' + let(:exists) { + Puppet::Provider::Confine::Exists + } + else + let(:exists) { + Puppet::Confine::Exists + } + end + + before :each do + # Reset the default provider + Puppet::Type.type(:firewallchain).defaultprovider = nil + + # Stub confine facts + allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') + end + + it "should default to iptables provider if /sbin/(eb|ip|ip6)tables[-save] exists" do + # Stub lookup for /sbin/iptables & /sbin/iptables-save + allow(exists).to receive(:which).with("ebtables"). + and_return "/sbin/ebtables" + allow(exists).to receive(:which).with("ebtables-save"). + and_return "/sbin/ebtables-save" + + allow(exists).to receive(:which).with("iptables"). + and_return "/sbin/iptables" + allow(exists).to receive(:which).with("iptables-save"). + and_return "/sbin/iptables-save" + + allow(exists).to receive(:which).with("ip6tables"). + and_return "/sbin/ip6tables" + allow(exists).to receive(:which).with("ip6tables-save"). + and_return "/sbin/ip6tables-save" + + # Every other command should return false so we don't pick up any + # other providers + allow(exists).to receive(:which) { |value| + value !~ /(eb|ip|ip6)tables(-save)?$/ + }.and_return false + + # Create a resource instance and make sure the provider is iptables + resource = Puppet::Type.type(:firewallchain).new({ + :name => 'test:filter:IPv4', + }) + expect(resource.provider.class.to_s).to eq("Puppet::Type::Firewallchain::ProviderIptables_chain") + end +end + +describe 'iptables chain provider' do + let(:provider) { Puppet::Type.type(:firewallchain).provider(:iptables_chain) } + let(:resource) { + Puppet::Type.type(:firewallchain).new({ + :name => ':test:', + }) + } + + before :each do + allow(Puppet::Type::Firewallchain).to receive(:defaultprovider).and_return provider + allow(provider).to receive(:command).with(:ebtables_save).and_return "/sbin/ebtables-save" + allow(provider).to receive(:command).with(:iptables_save).and_return "/sbin/iptables-save" + allow(provider).to receive(:command).with(:ip6tables_save).and_return "/sbin/ip6tables-save" + end + + it 'should be able to get a list of existing rules' do + # Pretend to return nil from iptables + allow(provider).to receive(:execute).with(['/sbin/ip6tables-save']).and_return("") + allow(provider).to receive(:execute).with(['/sbin/ebtables-save']).and_return("") + allow(provider).to receive(:execute).with(['/sbin/iptables-save']).and_return("") + + provider.instances.each do |chain| + expect(chain).to be_instance_of(provider) + expect(chain.properties[:provider].to_s).to eq(provider.name.to_s) + end + end + +end + +describe 'iptables chain resource parsing' do + let(:provider) { Puppet::Type.type(:firewallchain).provider(:iptables_chain) } + + before :each do + ebtables = ['BROUTE:BROUTING:ethernet', + 'BROUTE:broute:ethernet', + ':INPUT:ethernet', + ':FORWARD:ethernet', + ':OUTPUT:ethernet', + ':filter:ethernet', + ':filterdrop:ethernet', + ':filterreturn:ethernet', + 'NAT:PREROUTING:ethernet', + 'NAT:OUTPUT:ethernet', + 'NAT:POSTROUTING:ethernet', + ] + allow(provider).to receive(:execute).with(['/sbin/ebtables-save']).and_return(' +*broute +:BROUTING ACCEPT +:broute ACCEPT + +*filter +:INPUT ACCEPT +:FORWARD ACCEPT +:OUTPUT ACCEPT +:filter ACCEPT +:filterdrop DROP +:filterreturn RETURN + +*nat +:PREROUTING ACCEPT +:OUTPUT ACCEPT +:POSTROUTING ACCEPT +') + + iptables = [ + 'raw:PREROUTING:IPv4', + 'raw:OUTPUT:IPv4', + 'raw:raw:IPv4', + 'mangle:PREROUTING:IPv4', + 'mangle:INPUT:IPv4', + 'mangle:FORWARD:IPv4', + 'mangle:OUTPUT:IPv4', + 'mangle:POSTROUTING:IPv4', + 'mangle:mangle:IPv4', + 'NAT:PREROUTING:IPv4', + 'NAT:OUTPUT:IPv4', + 'NAT:POSTROUTING:IPv4', + 'NAT:mangle:IPv4', + 'NAT:mangle:IPv4', + 'NAT:mangle:IPv4', + ':$5()*&%\'"^$): :IPv4', + ] + allow(provider).to receive(:execute).with(['/sbin/iptables-save']).and_return(' +# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012 +*raw +:PREROUTING ACCEPT [12:1780] +:OUTPUT ACCEPT [19:1159] +:raw - [0:0] +COMMIT +# Completed on Mon Jan 2 01:20:06 2012 +# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012 +*mangle +:PREROUTING ACCEPT [12:1780] +:INPUT ACCEPT [12:1780] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [19:1159] +:POSTROUTING ACCEPT [19:1159] +:mangle - [0:0] +COMMIT +# Completed on Mon Jan 2 01:20:06 2012 +# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012 +*nat +:PREROUTING ACCEPT [2242:639750] +:OUTPUT ACCEPT [5176:326206] +:POSTROUTING ACCEPT [5162:325382] +COMMIT +# Completed on Mon Jan 2 01:20:06 2012 +# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012 +*filter +:INPUT ACCEPT [0:0] +:FORWARD DROP [0:0] +:OUTPUT ACCEPT [5673:420879] +:$5()*&%\'"^$): - [0:0] +COMMIT +# Completed on Mon Jan 2 01:20:06 2012 +') + ip6tables = [ + 'raw:PREROUTING:IPv6', + 'raw:OUTPUT:IPv6', + 'raw:ff:IPv6', + 'mangle:PREROUTING:IPv6', + 'mangle:INPUT:IPv6', + 'mangle:FORWARD:IPv6', + 'mangle:OUTPUT:IPv6', + 'mangle:POSTROUTING:IPv6', + 'mangle:ff:IPv6', + ':INPUT:IPv6', + ':FORWARD:IPv6', + ':OUTPUT:IPv6', + ':test:IPv6', + ] + allow(provider).to receive(:execute).with(['/sbin/ip6tables-save']).and_return(' +# Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012 +*raw +:PREROUTING ACCEPT [2173:489241] +:OUTPUT ACCEPT [0:0] +:ff - [0:0] +COMMIT +# Completed on Mon Jan 2 01:31:39 2012 +# Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012 +*mangle +:PREROUTING ACCEPT [2301:518373] +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +:ff - [0:0] +COMMIT +# Completed on Mon Jan 2 01:31:39 2012 +# Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012 +*filter +:INPUT ACCEPT [0:0] +:FORWARD DROP [0:0] +:OUTPUT ACCEPT [20:1292] +:test - [0:0] +COMMIT +# Completed on Mon Jan 2 01:31:39 2012 +') + @all = ebtables + iptables + ip6tables + # IPv4 and IPv6 names also exist as resources {table}:{chain}:IP and {table}:{chain}: + iptables.each { |name| @all += [ name[0..-3], name[0..-5] ] } + ip6tables.each { |name| @all += [ name[0..-3], name[0..-5] ] } + end + + it 'should have all in parsed resources' do + provider.instances.each do |resource| + @all.include?(resource.name) + end + end + +end diff --git a/firewall/spec/unit/puppet/provider/iptables_spec.rb b/firewall/spec/unit/puppet/provider/iptables_spec.rb new file mode 100644 index 000000000..e73bf84ad --- /dev/null +++ b/firewall/spec/unit/puppet/provider/iptables_spec.rb @@ -0,0 +1,435 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +if Puppet.version < '3.4.0' + require 'puppet/provider/confine/exists' +else + require 'puppet/confine/exists' +end + +describe 'iptables provider detection' do + if Puppet.version < '3.4.0' + let(:exists) { + Puppet::Provider::Confine::Exists + } + else + let(:exists) { + Puppet::Confine::Exists + } + end + + before :each do + # Reset the default provider + Puppet::Type.type(:firewall).defaultprovider = nil + + # Stub confine facts + allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') + end + + it "should default to iptables provider if /sbin/iptables[-save] exists" do + # Stub lookup for /sbin/iptables & /sbin/iptables-save + allow(exists).to receive(:which).with("iptables"). + and_return "/sbin/iptables" + allow(exists).to receive(:which).with("iptables-save"). + and_return "/sbin/iptables-save" + + # Every other command should return false so we don't pick up any + # other providers + allow(exists).to receive(:which) { |value| + ! ["iptables","iptables-save"].include?(value) + }.and_return false + + # Create a resource instance and make sure the provider is iptables + resource = Puppet::Type.type(:firewall).new({ + :name => '000 test foo', + }) + expect(resource.provider.class.to_s).to eq("Puppet::Type::Firewall::ProviderIptables") + end +end + +describe 'iptables provider' do + let(:provider) { Puppet::Type.type(:firewall).provider(:iptables) } + let(:resource) { + Puppet::Type.type(:firewall).new({ + :name => '000 test foo', + :action => 'accept', + }) + } + + before :each do + allow(Puppet::Type::Firewall).to receive(:defaultprovider).and_return provider + allow(provider).to receive(:command).with(:iptables_save).and_return "/sbin/iptables-save" + + # Stub iptables version + allow(Facter.fact(:iptables_version)).to receive(:value).and_return("1.4.2") + + allow(Puppet::Util::Execution).to receive(:execute).and_return "" + allow(Puppet::Util).to receive(:which).with("iptables-save"). + and_return "/sbin/iptables-save" + end + + it 'should be able to get a list of existing rules' do + provider.instances.each do |rule| + expect(rule).to be_instance_of(provider) + expect(rule.properties[:provider].to_s).to eq(provider.name.to_s) + end + end + + it 'should ignore lines with fatal errors' do + allow(Puppet::Util::Execution).to receive(:execute).with(['/sbin/iptables-save']). + and_return("FATAL: Could not load /lib/modules/2.6.18-028stab095.1/modules.dep: No such file or directory") + + expect(provider.instances.length).to be_zero + end + + describe '#insert_order' do + let(:iptables_save_output) { [ + '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT', + '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -m comment --comment "200 test" -j ACCEPT', + '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT' + ] } + let(:resources) do + iptables_save_output.each_with_index.collect { |l,index| provider.rule_to_hash(l, 'filter', index) } + end + let(:providers) do + resources.collect { |r| provider.new(r) } + end + it 'understands offsets for adding rules to the beginning' do + resource = Puppet::Type.type(:firewall).new({ :name => '001 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(1) # 1-indexed + end + it 'understands offsets for editing rules at the beginning' do + resource = Puppet::Type.type(:firewall).new({ :name => '100 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(1) + end + it 'understands offsets for adding rules to the middle' do + resource = Puppet::Type.type(:firewall).new({ :name => '101 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(2) + end + it 'understands offsets for editing rules at the middle' do + resource = Puppet::Type.type(:firewall).new({ :name => '200 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(2) + end + it 'understands offsets for adding rules to the end' do + resource = Puppet::Type.type(:firewall).new({ :name => '301 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(4) + end + it 'understands offsets for editing rules at the end' do + resource = Puppet::Type.type(:firewall).new({ :name => '300 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(3) + end + + context 'with unname rules between' do + let(:iptables_save_output) { [ + '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT', + '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 150 -m comment --comment "150 test" -j ACCEPT', + '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -j ACCEPT', + '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 250 -j ACCEPT', + '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT', + '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 350 -m comment --comment "350 test" -j ACCEPT', + ] } + it 'understands offsets for adding rules before unnamed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '001 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(1) + end + it 'understands offsets for editing rules before unnamed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '100 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(1) + end + it 'understands offsets for adding rules between managed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '120 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(2) + end + it 'understands offsets for adding rules between unnamed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '151 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(3) + end + it 'understands offsets for adding rules after unnamed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '351 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(7) + end + end + + context 'with unname rules before and after' do + let(:iptables_save_output) { [ + '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 050 -j ACCEPT', + '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 090 -j ACCEPT', + '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT', + '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 150 -m comment --comment "150 test" -j ACCEPT', + '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -j ACCEPT', + '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 250 -j ACCEPT', + '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT', + '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 350 -m comment --comment "350 test" -j ACCEPT', + '-A INPUT -s 8.0.0.5/32 -p tcp -m multiport --ports 400 -j ACCEPT', + '-A INPUT -s 8.0.0.5/32 -p tcp -m multiport --ports 450 -j ACCEPT', + ] } + it 'understands offsets for adding rules before unnamed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '001 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(1) + end + it 'understands offsets for editing rules before unnamed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '100 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(3) + end + it 'understands offsets for adding rules between managed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '120 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(4) + end + it 'understands offsets for adding rules between unnamed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '151 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(5) + end + it 'understands offsets for adding rules after unnamed rules' do + resource = Puppet::Type.type(:firewall).new({ :name => '351 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(9) + end + it 'understands offsets for adding rules at the end' do + resource = Puppet::Type.type(:firewall).new({ :name => '950 test', }) + allow(resource.provider.class).to receive(:instances).and_return(providers) + expect(resource.provider.insert_order).to eq(11) + end + end + end + + # Load in ruby hash for test fixtures. + load 'spec/fixtures/iptables/conversion_hash.rb' + + describe 'when converting rules to resources' do + ARGS_TO_HASH.each do |test_name,data| + describe "for test data '#{test_name}'" do + let(:resource) { provider.rule_to_hash(data[:line], data[:table], 0) } + + # If this option is enabled, make sure the parameters exactly match + if data[:compare_all] then + it "the parameter hash keys should be the same as returned by rules_to_hash" do + expect(resource.keys).to match_array(data[:params].keys) + end + end + + # Iterate across each parameter, creating an example for comparison + data[:params].each do |param_name, param_value| + it "the parameter '#{param_name.to_s}' should match #{param_value.inspect}" do + # booleans get cludged to string "true" + if param_value == true then + expect(resource[param_name]).to be_truthy + else + expect(resource[param_name]).to eq(data[:params][param_name]) + end + end + end + end + end + end + + describe 'when working out general_args' do + HASH_TO_ARGS.each do |test_name,data| + describe "for test data '#{test_name}'" do + let(:resource) { Puppet::Type.type(:firewall).new(data[:params]) } + let(:provider) { Puppet::Type.type(:firewall).provider(:iptables) } + let(:instance) { provider.new(resource) } + + it 'general_args should be valid' do + expect(instance.general_args.flatten).to eq(data[:args]) + end + end + end + end + + describe 'when converting rules without comments to resources' do + let(:sample_rule) { + '-A INPUT -s 1.1.1.1 -d 1.1.1.1 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -j ACCEPT' + } + let(:resource) { provider.rule_to_hash(sample_rule, 'filter', 0) } + let(:instance) { provider.new(resource) } + + it 'rule name contains a MD5 sum of the line' do + expect(resource[:name]).to eq("9000 #{Digest::MD5.hexdigest(resource[:line])}") + end + + it 'parsed the rule arguments correctly' do + expect(resource[:chain]).to eq('INPUT') + expect(resource[:source]).to eq('1.1.1.1/32') + expect(resource[:destination]).to eq('1.1.1.1/32') + expect(resource[:proto]).to eq('tcp') + expect(resource[:dport]).to eq(['7061', '7062']) + expect(resource[:sport]).to eq(['7061', '7062']) + expect(resource[:action]).to eq('accept') + end + end + + describe 'when converting existing rules generates by system-config-firewall-tui to resources' do + let(:sample_rule) { + # as generated by iptables-save from rules created with system-config-firewall-tui + '-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT' + } + let(:resource) { provider.rule_to_hash(sample_rule, 'filter', 0) } + let(:instance) { provider.new(resource) } + + it 'rule name contains a MD5 sum of the line' do + expect(resource[:name]).to eq("9000 #{Digest::MD5.hexdigest(resource[:line])}") + end + + it 'parse arguments' do + expect(resource[:chain]).to eq('INPUT') + expect(resource[:proto]).to eq('tcp') + expect(resource[:dport]).to eq(['22']) + expect(resource[:state]).to eq(['NEW']) + expect(resource[:action]).to eq('accept') + end + end + + describe 'when creating resources' do + let(:instance) { provider.new(resource) } + + it 'insert_args should be an array' do + expect(instance.insert_args.class).to eq(Array) + end + end + + describe 'when modifying resources' do + let(:instance) { provider.new(resource) } + + it 'update_args should be an array' do + expect(instance.update_args.class).to eq(Array) + end + + it 'fails when modifying the chain' do + expect { instance.chain = "OUTPUT" }.to raise_error(/is not supported/) + end + end + + describe 'when inverting rules' do + let(:resource) { + Puppet::Type.type(:firewall).new({ + :name => '040 partial invert', + :table => 'filter', + :action => 'accept', + :chain => 'nova-compute-FORWARD', + :source => '0.0.0.0/32', + :destination => '255.255.255.255/32', + :sport => ['! 78','79','http'], + :dport => ['77','! 76'], + :proto => 'udp', + }) + } + let(:instance) { provider.new(resource) } + + it 'fails when not all array items are inverted' do + expect { instance.insert }.to raise_error Puppet::Error, /but '79', '80' are not prefixed/ + end + end + + describe 'when deleting resources' do + let(:sample_rule) { + '-A INPUT -s 1.1.1.1 -d 1.1.1.1 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -j ACCEPT' + } + let(:resource) { provider.rule_to_hash(sample_rule, 'filter', 0) } + let(:instance) { provider.new(resource) } + + it 'resource[:line] looks like the original rule' do + resource[:line] == sample_rule + end + + it 'delete_args is an array' do + expect(instance.delete_args.class).to eq(Array) + end + + it 'delete_args is the same as the rule string when joined' do + expect(instance.delete_args.join(' ')).to eq(sample_rule.gsub(/\-A/, + '-t filter -D')) + end + end +end + +describe 'ip6tables provider' do + let(:provider6) { Puppet::Type.type(:firewall).provider(:ip6tables) } + let(:resource) { + Puppet::Type.type(:firewall).new({ + :name => '000 test foo', + :action => 'accept', + :provider => "ip6tables", + }) + } + + before :each do + allow(Puppet::Type::Firewall).to receive(:ip6tables).and_return provider6 + allow(provider6).to receive(:command).with(:ip6tables_save).and_return "/sbin/ip6tables-save" + + # Stub iptables version + allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return '1.4.7' + + allow(Puppet::Util::Execution).to receive(:execute).and_return '' + allow(Puppet::Util).to receive(:which).with("ip6tables-save"). + and_return "/sbin/ip6tables-save" + end + + it 'should be able to get a list of existing rules' do + provider6.instances.each do |rule| + rule.should be_instance_of(provider6) + rule.properties[:provider6].to_s.should == provider6.name.to_s + end + end + + it 'should ignore lines with fatal errors' do + allow(Puppet::Util::Execution).to receive(:execute).with(['/sbin/ip6tables-save']). + and_return("FATAL: Could not load /lib/modules/2.6.18-028stab095.1/modules.dep: No such file or directory") + provider6.instances.length.should == 0 + end + + # Load in ruby hash for test fixtures. + load 'spec/fixtures/ip6tables/conversion_hash.rb' + + describe 'when converting rules to resources' do + ARGS_TO_HASH6.each do |test_name,data| + describe "for test data '#{test_name}'" do + let(:resource) { provider6.rule_to_hash(data[:line], data[:table], 0) } + + # If this option is enabled, make sure the parameters exactly match + if data[:compare_all] then + it "the parameter hash keys should be the same as returned by rules_to_hash" do + resource.keys.should =~ data[:params].keys + end + end + + # Iterate across each parameter, creating an example for comparison + data[:params].each do |param_name, param_value| + it "the parameter '#{param_name.to_s}' should match #{param_value.inspect}" do + resource[param_name].should == data[:params][param_name] + end + end + end + end + end + + describe 'when working out general_args' do + HASH_TO_ARGS6.each do |test_name,data| + describe "for test data '#{test_name}'" do + let(:resource) { Puppet::Type.type(:firewall).new(data[:params]) } + let(:provider6) { Puppet::Type.type(:firewall).provider(:ip6tables) } + let(:instance) { provider6.new(resource) } + + it 'general_args should be valid' do + instance.general_args.flatten.should == data[:args] + end + end + end + end +end + diff --git a/firewall/spec/unit/puppet/type/firewall_spec.rb b/firewall/spec/unit/puppet/type/firewall_spec.rb new file mode 100755 index 000000000..8e9ef5663 --- /dev/null +++ b/firewall/spec/unit/puppet/type/firewall_spec.rb @@ -0,0 +1,672 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +firewall = Puppet::Type.type(:firewall) + +describe firewall do + before :each do + @class = firewall + @provider = double 'provider' + allow(@provider).to receive(:name).and_return(:iptables) + allow(Puppet::Type::Firewall).to receive(:defaultprovider).and_return @provider + + @resource = @class.new({:name => '000 test foo'}) + + # Stub iptables version + allow(Facter.fact(:iptables_version)).to receive(:value).and_return('1.4.2') + allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return('1.4.2') + + # Stub confine facts + allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') + end + + it 'should have :name be its namevar' do + @class.key_attributes.should == [:name] + end + + describe ':name' do + it 'should accept a name' do + @resource[:name] = '000-test-foo' + @resource[:name].should == '000-test-foo' + end + + it 'should not accept a name with non-ASCII chars' do + lambda { @resource[:name] = '%*#^(#$' }.should raise_error(Puppet::Error) + end + end + + describe ':action' do + it "should have no default" do + res = @class.new(:name => "000 test") + res.parameters[:action].should == nil + end + + [:accept, :drop, :reject].each do |action| + it "should accept value #{action}" do + @resource[:action] = action + @resource[:action].should == action + end + end + + it 'should fail when value is not recognized' do + lambda { @resource[:action] = 'not valid' }.should raise_error(Puppet::Error) + end + end + + describe ':chain' do + [:INPUT, :FORWARD, :OUTPUT, :PREROUTING, :POSTROUTING].each do |chain| + it "should accept chain value #{chain}" do + @resource[:chain] = chain + @resource[:chain].should == chain + end + end + + it 'should fail when the chain value is not recognized' do + lambda { @resource[:chain] = 'not valid' }.should raise_error(Puppet::Error) + end + end + + describe ':table' do + [:nat, :mangle, :filter, :raw].each do |table| + it "should accept table value #{table}" do + @resource[:table] = table + @resource[:table].should == table + end + end + + it "should fail when table value is not recognized" do + lambda { @resource[:table] = 'not valid' }.should raise_error(Puppet::Error) + end + end + + describe ':proto' do + [:tcp, :udp, :icmp, :esp, :ah, :vrrp, :igmp, :ipencap, :ospf, :gre, :all].each do |proto| + it "should accept proto value #{proto}" do + @resource[:proto] = proto + @resource[:proto].should == proto + end + end + + it "should fail when proto value is not recognized" do + lambda { @resource[:proto] = 'foo' }.should raise_error(Puppet::Error) + end + end + + describe ':jump' do + it "should have no default" do + res = @class.new(:name => "000 test") + res.parameters[:jump].should == nil + end + + ['QUEUE', 'RETURN', 'DNAT', 'SNAT', 'LOG', 'MASQUERADE', 'REDIRECT', 'MARK'].each do |jump| + it "should accept jump value #{jump}" do + @resource[:jump] = jump + @resource[:jump].should == jump + end + end + + ['ACCEPT', 'DROP', 'REJECT'].each do |jump| + it "should now fail when value #{jump}" do + lambda { @resource[:jump] = jump }.should raise_error(Puppet::Error) + end + end + + it "should fail when jump value is not recognized" do + lambda { @resource[:jump] = '%^&*' }.should raise_error(Puppet::Error) + end + end + + [:source, :destination].each do |addr| + describe addr do + it "should accept a #{addr} as a string" do + @resource[addr] = '127.0.0.1' + @resource[addr].should == '127.0.0.1/32' + end + ['0.0.0.0/0', '::/0'].each do |prefix| + it "should be nil for zero prefix length address #{prefix}" do + @resource[addr] = prefix + @resource[addr].should == nil + end + end + it "should accept a negated #{addr} as a string" do + @resource[addr] = '! 127.0.0.1' + @resource[addr].should == '! 127.0.0.1/32' + end + end + end + + [:dport, :sport].each do |port| + describe port do + it "should accept a #{port} as string" do + @resource[port] = '22' + @resource[port].should == ['22'] + end + + it "should accept a #{port} as an array" do + @resource[port] = ['22','23'] + @resource[port].should == ['22','23'] + end + + it "should accept a #{port} as a number" do + @resource[port] = 22 + @resource[port].should == ['22'] + end + + it "should accept a #{port} as a hyphen separated range" do + @resource[port] = ['22-1000'] + @resource[port].should == ['22-1000'] + end + + it "should accept a #{port} as a combination of arrays of single and " \ + "hyphen separated ranges" do + + @resource[port] = ['22-1000','33','3000-4000'] + @resource[port].should == ['22-1000','33','3000-4000'] + end + + it "should convert a port name for #{port} to its number" do + @resource[port] = 'ssh' + @resource[port].should == ['22'] + end + + it "should not accept something invalid for #{port}" do + expect { @resource[port] = 'something odd' }.to raise_error(Puppet::Error, /^Parameter .+ failed.+Munging failed for value ".+" in class .+: no such service/) + end + + it "should not accept something invalid in an array for #{port}" do + expect { @resource[port] = ['something odd','something even odder'] }.to raise_error(Puppet::Error, /^Parameter .+ failed.+Munging failed for value ".+" in class .+: no such service/) + end + end + end + + [:dst_type, :src_type].each do |addrtype| + describe addrtype do + it "should have no default" do + res = @class.new(:name => "000 test") + res.parameters[addrtype].should == nil + end + end + + [:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, :BLACKHOLE, + :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].each do |type| + it "should accept #{addrtype} value #{type}" do + @resource[addrtype] = type + @resource[addrtype].should == type + end + end + + it "should fail when #{addrtype} value is not recognized" do + lambda { @resource[addrtype] = 'foo' }.should raise_error(Puppet::Error) + end + end + + [:iniface, :outiface].each do |iface| + describe iface do + it "should accept #{iface} value as a string" do + @resource[iface] = 'eth1' + @resource[iface].should == 'eth1' + end + end + end + + [:tosource, :todest].each do |addr| + describe addr do + it "should accept #{addr} value as a string" do + @resource[addr] = '127.0.0.1' + end + end + end + + describe ':log_level' do + values = { + 'panic' => '0', + 'alert' => '1', + 'crit' => '2', + 'err' => '3', + 'warn' => '4', + 'warning' => '4', + 'not' => '5', + 'notice' => '5', + 'info' => '6', + 'debug' => '7' + } + + values.each do |k,v| + it { + @resource[:log_level] = k + @resource[:log_level].should == v + } + + it { + @resource[:log_level] = 3 + @resource[:log_level].should == 3 + } + + it { lambda { @resource[:log_level] = 'foo' }.should raise_error(Puppet::Error) } + end + end + + describe ':icmp' do + icmp_codes = { + :iptables => { + '0' => 'echo-reply', + '3' => 'destination-unreachable', + '4' => 'source-quench', + '6' => 'redirect', + '8' => 'echo-request', + '9' => 'router-advertisement', + '10' => 'router-solicitation', + '11' => 'time-exceeded', + '12' => 'parameter-problem', + '13' => 'timestamp-request', + '14' => 'timestamp-reply', + '17' => 'address-mask-request', + '18' => 'address-mask-reply' + }, + :ip6tables => { + '1' => 'destination-unreachable', + '3' => 'time-exceeded', + '4' => 'parameter-problem', + '128' => 'echo-request', + '129' => 'echo-reply', + '133' => 'router-solicitation', + '134' => 'router-advertisement', + '137' => 'redirect' + } + } + icmp_codes.each do |provider, values| + describe provider do + values.each do |k,v| + it 'should convert icmp string to number' do + @resource[:provider] = provider + @resource[:provider].should == provider + @resource[:icmp] = v + @resource[:icmp].should == k + end + end + end + end + + it 'should accept values as integers' do + @resource[:icmp] = 9 + @resource[:icmp].should == 9 + end + + it 'should fail if icmp type is "any"' do + lambda { @resource[:icmp] = 'any' }.should raise_error(Puppet::Error) + end + + it 'should fail if icmp type cannot be mapped to a numeric' do + lambda { @resource[:icmp] = 'foo' }.should raise_error(Puppet::Error) + end + end + + describe ':state' do + it 'should accept value as a string' do + @resource[:state] = :INVALID + @resource[:state].should == [:INVALID] + end + + it 'should accept value as an array' do + @resource[:state] = [:INVALID, :NEW] + @resource[:state].should == [:INVALID, :NEW] + end + + it 'should sort values alphabetically' do + @resource[:state] = [:NEW, :ESTABLISHED] + @resource[:state].should == [:ESTABLISHED, :NEW] + end + end + + describe ':ctstate' do + it 'should accept value as a string' do + @resource[:ctstate] = :INVALID + @resource[:ctstate].should == [:INVALID] + end + + it 'should accept value as an array' do + @resource[:ctstate] = [:INVALID, :NEW] + @resource[:ctstate].should == [:INVALID, :NEW] + end + + it 'should sort values alphabetically' do + @resource[:ctstate] = [:NEW, :ESTABLISHED] + @resource[:ctstate].should == [:ESTABLISHED, :NEW] + end + end + + describe ':burst' do + it 'should accept numeric values' do + @resource[:burst] = 12 + @resource[:burst].should == 12 + end + + it 'should fail if value is not numeric' do + lambda { @resource[:burst] = 'foo' }.should raise_error(Puppet::Error) + end + end + + describe ':recent' do + ['set', 'update', 'rcheck', 'remove'].each do |recent| + it "should accept recent value #{recent}" do + @resource[:recent] = recent + @resource[:recent].should == "--#{recent}" + end + end + end + + describe ':action and :jump' do + it 'should allow only 1 to be set at a time' do + expect { + @class.new( + :name => "001-test", + :action => "accept", + :jump => "custom_chain" + ) + }.to raise_error(Puppet::Error, /Only one of the parameters 'action' and 'jump' can be set$/) + end + end + describe ':gid and :uid' do + it 'should allow me to set uid' do + @resource[:uid] = 'root' + @resource[:uid].should == 'root' + end + it 'should allow me to set uid as an array, and silently hide my error' do + @resource[:uid] = ['root', 'bobby'] + @resource[:uid].should == 'root' + end + it 'should allow me to set gid' do + @resource[:gid] = 'root' + @resource[:gid].should == 'root' + end + it 'should allow me to set gid as an array, and silently hide my error' do + @resource[:gid] = ['root', 'bobby'] + @resource[:gid].should == 'root' + end + end + + describe ':set_mark' do + ['1.3.2', '1.4.2'].each do |iptables_version| + describe "with iptables #{iptables_version}" do + before { + Facter.clear + allow(Facter.fact(:iptables_version)).to receive(:value).and_return iptables_version + allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return iptables_version + } + + if iptables_version == '1.3.2' + it 'should allow me to set set-mark without mask' do + @resource[:set_mark] = '0x3e8' + @resource[:set_mark].should == '0x3e8' + end + it 'should convert int to hex without mask' do + @resource[:set_mark] = '1000' + @resource[:set_mark].should == '0x3e8' + end + it 'should fail if mask is present' do + lambda { @resource[:set_mark] = '0x3e8/0xffffffff'}.should raise_error( + Puppet::Error, /iptables version #{iptables_version} does not support masks on MARK rules$/ + ) + end + end + + if iptables_version == '1.4.2' + it 'should allow me to set set-mark with mask' do + @resource[:set_mark] = '0x3e8/0xffffffff' + @resource[:set_mark].should == '0x3e8/0xffffffff' + end + it 'should convert int to hex and add a 32 bit mask' do + @resource[:set_mark] = '1000' + @resource[:set_mark].should == '0x3e8/0xffffffff' + end + it 'should add a 32 bit mask' do + @resource[:set_mark] = '0x32' + @resource[:set_mark].should == '0x32/0xffffffff' + end + it 'should use the mask provided' do + @resource[:set_mark] = '0x32/0x4' + @resource[:set_mark].should == '0x32/0x4' + end + it 'should use the mask provided and convert int to hex' do + @resource[:set_mark] = '1000/0x4' + @resource[:set_mark].should == '0x3e8/0x4' + end + it 'should fail if mask value is more than 32 bits' do + lambda { @resource[:set_mark] = '1/4294967296'}.should raise_error( + Puppet::Error, /MARK mask must be integer or hex between 0 and 0xffffffff$/ + ) + end + it 'should fail if mask is malformed' do + lambda { @resource[:set_mark] = '1000/0xq4'}.should raise_error( + Puppet::Error, /MARK mask must be integer or hex between 0 and 0xffffffff$/ + ) + end + end + + ['/', '1000/', 'pwnie'].each do |bad_mark| + it "should fail with malformed mark '#{bad_mark}'" do + lambda { @resource[:set_mark] = bad_mark}.should raise_error(Puppet::Error) + end + end + it 'should fail if mark value is more than 32 bits' do + lambda { @resource[:set_mark] = '4294967296'}.should raise_error( + Puppet::Error, /MARK value must be integer or hex between 0 and 0xffffffff$/ + ) + end + end + end + end + + [:chain, :jump].each do |param| + describe param do + it 'should autorequire fwchain when table and provider are undefined' do + @resource[param] = 'FOO' + @resource[:table].should == :filter + @resource[:provider].should == :iptables + + chain = Puppet::Type.type(:firewallchain).new(:name => 'FOO:filter:IPv4') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain + rel = @resource.autorequire[0] + rel.source.ref.should == chain.ref + rel.target.ref.should == @resource.ref + end + + it 'should autorequire fwchain when table is undefined and provider is ip6tables' do + @resource[param] = 'FOO' + @resource[:table].should == :filter + @resource[:provider] = :ip6tables + + chain = Puppet::Type.type(:firewallchain).new(:name => 'FOO:filter:IPv6') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain + rel = @resource.autorequire[0] + rel.source.ref.should == chain.ref + rel.target.ref.should == @resource.ref + end + + it 'should autorequire fwchain when table is raw and provider is undefined' do + @resource[param] = 'FOO' + @resource[:table] = :raw + @resource[:provider].should == :iptables + + chain = Puppet::Type.type(:firewallchain).new(:name => 'FOO:raw:IPv4') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain + rel = @resource.autorequire[0] + rel.source.ref.should == chain.ref + rel.target.ref.should == @resource.ref + end + + it 'should autorequire fwchain when table is raw and provider is ip6tables' do + @resource[param] = 'FOO' + @resource[:table] = :raw + @resource[:provider] = :ip6tables + + chain = Puppet::Type.type(:firewallchain).new(:name => 'FOO:raw:IPv6') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain + rel = @resource.autorequire[0] + rel.source.ref.should == chain.ref + rel.target.ref.should == @resource.ref + end + + # test where autorequire is still needed (table != filter) + ['INPUT', 'OUTPUT', 'FORWARD'].each do |test_chain| + it "should autorequire fwchain #{test_chain} when table is mangle and provider is undefined" do + @resource[param] = test_chain + @resource[:table] = :mangle + @resource[:provider].should == :iptables + + chain = Puppet::Type.type(:firewallchain).new(:name => "#{test_chain}:mangle:IPv4") + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain + rel = @resource.autorequire[0] + rel.source.ref.should == chain.ref + rel.target.ref.should == @resource.ref + end + + it "should autorequire fwchain #{test_chain} when table is mangle and provider is ip6tables" do + @resource[param] = test_chain + @resource[:table] = :mangle + @resource[:provider] = :ip6tables + + chain = Puppet::Type.type(:firewallchain).new(:name => "#{test_chain}:mangle:IPv6") + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain + rel = @resource.autorequire[0] + rel.source.ref.should == chain.ref + rel.target.ref.should == @resource.ref + end + end + + # test of case where autorequire should not happen + ['INPUT', 'OUTPUT', 'FORWARD'].each do |test_chain| + + it "should not autorequire fwchain #{test_chain} when table and provider are undefined" do + @resource[param] = test_chain + @resource[:table].should == :filter + @resource[:provider].should == :iptables + + chain = Puppet::Type.type(:firewallchain).new(:name => "#{test_chain}:filter:IPv4") + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain + rel = @resource.autorequire[0] + rel.should == nil + end + + it "should not autorequire fwchain #{test_chain} when table is undefined and provider is ip6tables" do + @resource[param] = test_chain + @resource[:table].should == :filter + @resource[:provider] = :ip6tables + + chain = Puppet::Type.type(:firewallchain).new(:name => "#{test_chain}:filter:IPv6") + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain + rel = @resource.autorequire[0] + rel.should == nil + end + end + end + end + + describe ":chain and :jump" do + it 'should autorequire independent fwchains' do + @resource[:chain] = 'FOO' + @resource[:jump] = 'BAR' + @resource[:table].should == :filter + @resource[:provider].should == :iptables + + chain_foo = Puppet::Type.type(:firewallchain).new(:name => 'FOO:filter:IPv4') + chain_bar = Puppet::Type.type(:firewallchain).new(:name => 'BAR:filter:IPv4') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource chain_foo + catalog.add_resource chain_bar + rel = @resource.autorequire + rel[0].source.ref.should == chain_foo.ref + rel[0].target.ref.should == @resource.ref + rel[1].source.ref.should == chain_bar.ref + rel[1].target.ref.should == @resource.ref + end + end + + describe ':pkttype' do + [:multicast, :broadcast, :unicast].each do |pkttype| + it "should accept pkttype value #{pkttype}" do + @resource[:pkttype] = pkttype + @resource[:pkttype].should == pkttype + end + end + + it 'should fail when the pkttype value is not recognized' do + lambda { @resource[:pkttype] = 'not valid' }.should raise_error(Puppet::Error) + end + end + + describe 'autorequire packages' do + [:iptables, :ip6tables].each do |provider| + it "provider #{provider} should autorequire package iptables" do + @resource[:provider] = provider + @resource[:provider].should == provider + package = Puppet::Type.type(:package).new(:name => 'iptables') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + catalog.add_resource package + rel = @resource.autorequire[0] + rel.source.ref.should == package.ref + rel.target.ref.should == @resource.ref + end + + it "provider #{provider} should autorequire packages iptables, iptables-persistent, and iptables-services" do + @resource[:provider] = provider + @resource[:provider].should == provider + packages = [ + Puppet::Type.type(:package).new(:name => 'iptables'), + Puppet::Type.type(:package).new(:name => 'iptables-persistent'), + Puppet::Type.type(:package).new(:name => 'iptables-services') + ] + catalog = Puppet::Resource::Catalog.new + catalog.add_resource @resource + packages.each do |package| + catalog.add_resource package + end + packages.zip(@resource.autorequire) do |package, rel| + rel.source.ref.should == package.ref + rel.target.ref.should == @resource.ref + end + end + end + end + it 'is suitable' do + expect(@resource.suitable?).to be_truthy + end +end + +describe 'firewall on unsupported platforms' do + it 'is not suitable' do + # Stub iptables version + allow(Facter.fact(:iptables_version)).to receive(:value).and_return(nil) + allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return(nil) + + # Stub confine facts + allow(Facter.fact(:kernel)).to receive(:value).and_return('Darwin') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Darwin') + resource = firewall.new(:name => "000 test foo", :ensure => :present) + + # If our provider list is nil, then the Puppet::Transaction#evaluate will + # say 'Error: Could not find a suitable provider for firewall' but there + # isn't a unit testable way to get this. + expect(resource.suitable?).to be_falsey + end +end diff --git a/firewall/spec/unit/puppet/type/firewallchain_spec.rb b/firewall/spec/unit/puppet/type/firewallchain_spec.rb new file mode 100755 index 000000000..bd3095e09 --- /dev/null +++ b/firewall/spec/unit/puppet/type/firewallchain_spec.rb @@ -0,0 +1,207 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +firewallchain = Puppet::Type.type(:firewallchain) + +describe firewallchain do + before(:each) do + # Stub confine facts + allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') + end + let(:klass) { firewallchain } + let(:provider) { + prov = double 'provider' + allow(prov).to receive(:name).and_return(:iptables_chain) + prov + } + let(:resource) { + allow(Puppet::Type::Firewallchain).to receive(:defaultprovider).and_return provider + klass.new({:name => 'INPUT:filter:IPv4', :policy => :accept }) + } + + it 'should have :name be its namevar' do + klass.key_attributes.should == [:name] + end + + describe ':name' do + {'nat' => ['PREROUTING', 'POSTROUTING', 'INPUT', 'OUTPUT'], + 'mangle' => [ 'PREROUTING', 'POSTROUTING', 'INPUT', 'FORWARD', 'OUTPUT' ], + 'filter' => ['INPUT','OUTPUT','FORWARD'], + 'raw' => [ 'PREROUTING', 'OUTPUT'], + 'broute' => ['BROUTING'] + }.each_pair do |table, allowedinternalchains| + ['IPv4', 'IPv6', 'ethernet'].each do |protocol| + [ 'test', '$5()*&%\'"^$09):' ].each do |chainname| + name = "#{chainname}:#{table}:#{protocol}" + if table == 'nat' && protocol == 'IPv6' + it "should fail #{name}" do + expect { resource[:name] = name }.to raise_error(Puppet::Error) + end + elsif protocol != 'ethernet' && table == 'broute' + it "should fail #{name}" do + expect { resource[:name] = name }.to raise_error(Puppet::Error) + end + else + it "should accept name #{name}" do + resource[:name] = name + resource[:name].should == name + end + end + end # chainname + end # protocol + + [ 'PREROUTING', 'POSTROUTING', 'BROUTING', 'INPUT', 'FORWARD', 'OUTPUT' ].each do |internalchain| + name = internalchain + ':' + table + ':' + if internalchain == 'BROUTING' + name += 'ethernet' + elsif table == 'nat' + name += 'IPv4' + else + name += 'IPv4' + end + if allowedinternalchains.include? internalchain + it "should allow #{name}" do + resource[:name] = name + resource[:name].should == name + end + else + it "should fail #{name}" do + expect { resource[:name] = name }.to raise_error(Puppet::Error) + end + end + end # internalchain + + end # table, allowedinternalchainnames + + it 'should fail with invalid table names' do + expect { resource[:name] = 'wrongtablename:test:IPv4' }.to raise_error(Puppet::Error) + end + + it 'should fail with invalid protocols names' do + expect { resource[:name] = 'test:filter:IPv5' }.to raise_error(Puppet::Error) + end + + end + + describe ':policy' do + + [:accept, :drop, :queue, :return].each do |policy| + it "should accept policy #{policy}" do + resource[:policy] = policy + resource[:policy].should == policy + end + end + + it 'should fail when value is not recognized' do + expect { resource[:policy] = 'not valid' }.to raise_error(Puppet::Error) + end + + [:accept, :drop, :queue, :return].each do |policy| + it "non-inbuilt chains should not accept policy #{policy}" do + expect { klass.new({:name => 'testchain:filter:IPv4', :policy => policy }) }.to raise_error(Puppet::Error) + end + it "non-inbuilt chains can accept policies on protocol = ethernet (policy #{policy})" do + klass.new({:name => 'testchain:filter:ethernet', :policy => policy }) + end + end + + end + + describe 'autorequire packages' do + it "provider iptables_chain should autorequire package iptables" do + resource[:provider].should == :iptables_chain + package = Puppet::Type.type(:package).new(:name => 'iptables') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource package + rel = resource.autorequire[0] + rel.source.ref.should == package.ref + rel.target.ref.should == resource.ref + end + + it "provider iptables_chain should autorequire packages iptables, iptables-persistent, and iptables-services" do + resource[:provider].should == :iptables_chain + packages = [ + Puppet::Type.type(:package).new(:name => 'iptables'), + Puppet::Type.type(:package).new(:name => 'iptables-persistent'), + Puppet::Type.type(:package).new(:name => 'iptables-services') + ] + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + packages.each do |package| + catalog.add_resource package + end + packages.zip(resource.autorequire) do |package, rel| + rel.source.ref.should == package.ref + rel.target.ref.should == resource.ref + end + end + end + + describe 'purge iptables rules' do + before(:each) do + allow(Puppet::Type.type(:firewall).provider(:iptables)).to receive(:iptables_save).and_return(< 'INPUT:filter:IPv4', :purge => true) + + expect(resource.generate.size).to eq(3) + end + + it 'should not generate ignored iptables rules' do + resource = Puppet::Type::Firewallchain.new(:name => 'INPUT:filter:IPv4', :purge => true, :ignore => ['-j fail2ban-ssh']) + + expect(resource.generate.size).to eq(2) + end + + it 'should not generate iptables resources when not enabled' do + resource = Puppet::Type::Firewallchain.new(:name => 'INPUT:filter:IPv4') + + expect(resource.generate.size).to eq(0) + end + end + it 'is suitable' do + expect(resource.suitable?).to be_truthy + end +end + +describe 'firewall on unsupported platforms' do + it 'is not suitable' do + # Stub iptables version + allow(Facter.fact(:iptables_version)).to receive(:value).and_return(nil) + allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return(nil) + + # Stub confine facts + allow(Facter.fact(:kernel)).to receive(:value).and_return('Darwin') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Darwin') + resource = firewallchain.new(:name => "INPUT:filter:IPv4", :ensure => :present) + + # If our provider list is nil, then the Puppet::Transaction#evaluate will + # say 'Error: Could not find a suitable provider for firewall' but there + # isn't a unit testable way to get this. + expect(resource.suitable?).to be_falsey + end +end diff --git a/firewall/spec/unit/puppet/util/firewall_spec.rb b/firewall/spec/unit/puppet/util/firewall_spec.rb new file mode 100644 index 000000000..e5864879c --- /dev/null +++ b/firewall/spec/unit/puppet/util/firewall_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' + +describe 'Puppet::Util::Firewall' do + let(:resource) { + type = Puppet::Type.type(:firewall) + provider = double 'provider' + allow(provider).to receive(:name).and_return(:iptables) + allow(Puppet::Type::Firewall).to receive(:defaultprovider).and_return(provider) + type.new({:name => '000 test foo'}) + } + + before(:each) { resource } + + describe '#host_to_ip' do + subject { resource } + specify { + expect(Resolv).to receive(:getaddress).with('puppetlabs.com').and_return('96.126.112.51') + subject.host_to_ip('puppetlabs.com').should == '96.126.112.51/32' + } + specify { subject.host_to_ip('96.126.112.51').should == '96.126.112.51/32' } + specify { subject.host_to_ip('96.126.112.51/32').should == '96.126.112.51/32' } + specify { subject.host_to_ip('2001:db8:85a3:0:0:8a2e:370:7334').should == '2001:db8:85a3::8a2e:370:7334/128' } + specify { subject.host_to_ip('2001:db8:1234::/48').should == '2001:db8:1234::/48' } + specify { subject.host_to_ip('0.0.0.0/0').should == nil } + specify { subject.host_to_ip('::/0').should == nil } + end + + describe '#host_to_mask' do + subject { resource } + specify { + expect(Resolv).to receive(:getaddress).at_least(:once).with('puppetlabs.com').and_return('96.126.112.51') + subject.host_to_mask('puppetlabs.com').should == '96.126.112.51/32' + subject.host_to_mask('!puppetlabs.com').should == '! 96.126.112.51/32' + } + specify { subject.host_to_mask('96.126.112.51').should == '96.126.112.51/32' } + specify { subject.host_to_mask('!96.126.112.51').should == '! 96.126.112.51/32' } + specify { subject.host_to_mask('96.126.112.51/32').should == '96.126.112.51/32' } + specify { subject.host_to_mask('! 96.126.112.51/32').should == '! 96.126.112.51/32' } + specify { subject.host_to_mask('2001:db8:85a3:0:0:8a2e:370:7334').should == '2001:db8:85a3::8a2e:370:7334/128' } + specify { subject.host_to_mask('!2001:db8:85a3:0:0:8a2e:370:7334').should == '! 2001:db8:85a3::8a2e:370:7334/128' } + specify { subject.host_to_mask('2001:db8:1234::/48').should == '2001:db8:1234::/48' } + specify { subject.host_to_mask('! 2001:db8:1234::/48').should == '! 2001:db8:1234::/48' } + specify { subject.host_to_mask('0.0.0.0/0').should == nil } + specify { subject.host_to_mask('!0.0.0.0/0').should == nil } + specify { subject.host_to_mask('::/0').should == nil } + specify { subject.host_to_mask('! ::/0').should == nil } + end + + describe '#icmp_name_to_number' do + describe 'proto unsupported' do + subject { resource } + + %w{inet5 inet8 foo}.each do |proto| + it "should reject invalid proto #{proto}" do + expect { subject.icmp_name_to_number('echo-reply', proto) }. + to raise_error(ArgumentError, "unsupported protocol family '#{proto}'") + end + end + end + + describe 'proto IPv4' do + proto = 'inet' + subject { resource } + specify { subject.icmp_name_to_number('echo-reply', proto).should == '0' } + specify { subject.icmp_name_to_number('destination-unreachable', proto).should == '3' } + specify { subject.icmp_name_to_number('source-quench', proto).should == '4' } + specify { subject.icmp_name_to_number('redirect', proto).should == '6' } + specify { subject.icmp_name_to_number('echo-request', proto).should == '8' } + specify { subject.icmp_name_to_number('router-advertisement', proto).should == '9' } + specify { subject.icmp_name_to_number('router-solicitation', proto).should == '10' } + specify { subject.icmp_name_to_number('time-exceeded', proto).should == '11' } + specify { subject.icmp_name_to_number('parameter-problem', proto).should == '12' } + specify { subject.icmp_name_to_number('timestamp-request', proto).should == '13' } + specify { subject.icmp_name_to_number('timestamp-reply', proto).should == '14' } + specify { subject.icmp_name_to_number('address-mask-request', proto).should == '17' } + specify { subject.icmp_name_to_number('address-mask-reply', proto).should == '18' } + end + + describe 'proto IPv6' do + proto = 'inet6' + subject { resource } + specify { subject.icmp_name_to_number('destination-unreachable', proto).should == '1' } + specify { subject.icmp_name_to_number('time-exceeded', proto).should == '3' } + specify { subject.icmp_name_to_number('parameter-problem', proto).should == '4' } + specify { subject.icmp_name_to_number('echo-request', proto).should == '128' } + specify { subject.icmp_name_to_number('echo-reply', proto).should == '129' } + specify { subject.icmp_name_to_number('router-solicitation', proto).should == '133' } + specify { subject.icmp_name_to_number('router-advertisement', proto).should == '134' } + specify { subject.icmp_name_to_number('redirect', proto).should == '137' } + end + end + + describe '#string_to_port' do + subject { resource } + specify { subject.string_to_port('80','tcp').should == '80' } + specify { subject.string_to_port(80,'tcp').should == '80' } + specify { subject.string_to_port('http','tcp').should == '80' } + specify { subject.string_to_port('domain','udp').should == '53' } + end + + describe '#to_hex32' do + subject { resource } + specify { subject.to_hex32('0').should == '0x0' } + specify { subject.to_hex32('0x32').should == '0x32' } + specify { subject.to_hex32('42').should == '0x2a' } + specify { subject.to_hex32('4294967295').should == '0xffffffff' } + specify { subject.to_hex32('4294967296').should == nil } + specify { subject.to_hex32('-1').should == nil } + specify { subject.to_hex32('bananas').should == nil } + end + + describe '#persist_iptables' do + before { Facter.clear } + subject { resource } + + describe 'when proto is IPv4' do + let(:proto) { 'IPv4' } + + it 'should exec /sbin/service if running RHEL 6 or earlier' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return('RedHat') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('RedHat') + allow(Facter.fact(:operatingsystemrelease)).to receive(:value).and_return('6') + + expect(subject).to receive(:execute).with(%w{/sbin/service iptables save}) + subject.persist_iptables(proto) + end + + it 'should exec for systemd if running RHEL 7 or greater' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return('RedHat') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('RedHat') + allow(Facter.fact(:operatingsystemrelease)).to receive(:value).and_return('7') + + expect(subject).to receive(:execute).with(%w{/usr/libexec/iptables/iptables.init save}) + subject.persist_iptables(proto) + end + + it 'should exec for systemd if running Fedora 15 or greater' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return('RedHat') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Fedora') + allow(Facter.fact(:operatingsystemrelease)).to receive(:value).and_return('15') + + expect(subject).to receive(:execute).with(%w{/usr/libexec/iptables/iptables.init save}) + subject.persist_iptables(proto) + end + + it 'should exec for CentOS identified from operatingsystem' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return(nil) + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('CentOS') + expect(subject).to receive(:execute).with(%w{/sbin/service iptables save}) + subject.persist_iptables(proto) + end + + it 'should exec for Archlinux identified from osfamily' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return('Archlinux') + expect(subject).to receive(:execute).with(['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/iptables/iptables.rules']) + subject.persist_iptables(proto) + end + + it 'should raise a warning when exec fails' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return('RedHat') + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('RedHat') + allow(Facter.fact(:operatingsystemrelease)).to receive(:value).and_return('6') + + expect(subject).to receive(:execute).with(%w{/sbin/service iptables save}). + and_raise(Puppet::ExecutionFailure, 'some error') + expect(subject).to receive(:warning).with('Unable to persist firewall rules: some error') + subject.persist_iptables(proto) + end + end + + describe 'when proto is IPv6' do + let(:proto) { 'IPv6' } + + it 'should exec for newer Ubuntu' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return(nil) + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Ubuntu') + allow(Facter.fact(:iptables_persistent_version)).to receive(:value).and_return('0.5.3ubuntu2') + expect(subject).to receive(:execute).with(%w{/usr/sbin/service iptables-persistent save}) + subject.persist_iptables(proto) + end + + it 'should not exec for older Ubuntu which does not support IPv6' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return(nil) + allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Ubuntu') + allow(Facter.fact(:iptables_persistent_version)).to receive(:value).and_return('0.0.20090701') + expect(subject).to receive(:execute).never + subject.persist_iptables(proto) + end + + it 'should not exec for Suse which is not supported' do + allow(Facter.fact(:osfamily)).to receive(:value).and_return('Suse') + expect(subject).to receive(:execute).never + subject.persist_iptables(proto) + end + end + end +end diff --git a/firewall/spec/unit/puppet/util/ipcidr_spec.rb b/firewall/spec/unit/puppet/util/ipcidr_spec.rb new file mode 100644 index 000000000..916f74a35 --- /dev/null +++ b/firewall/spec/unit/puppet/util/ipcidr_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe 'Puppet::Util::IPCidr' do + describe 'ipv4 address' do + before { @ipaddr = Puppet::Util::IPCidr.new('96.126.112.51') } + subject { @ipaddr } + specify { subject.cidr.should == '96.126.112.51/32' } + specify { subject.prefixlen.should == 32 } + specify { subject.netmask.should == '255.255.255.255' } + end + + describe 'single ipv4 address with cidr' do + before { @ipcidr = Puppet::Util::IPCidr.new('96.126.112.51/32') } + subject { @ipcidr } + specify { subject.cidr.should == '96.126.112.51/32' } + specify { subject.prefixlen.should == 32 } + specify { subject.netmask.should == '255.255.255.255' } + end + + describe 'ipv4 address range with cidr' do + before { @ipcidr = Puppet::Util::IPCidr.new('96.126.112.0/24') } + subject { @ipcidr } + specify { subject.cidr.should == '96.126.112.0/24' } + specify { subject.prefixlen.should == 24 } + specify { subject.netmask.should == '255.255.255.0' } + end + + describe 'ipv4 open range with cidr' do + before { @ipcidr = Puppet::Util::IPCidr.new('0.0.0.0/0') } + subject { @ipcidr } + specify { subject.cidr.should == '0.0.0.0/0' } + specify { subject.prefixlen.should == 0 } + specify { subject.netmask.should == '0.0.0.0' } + end + + describe 'ipv6 address' do + before { @ipaddr = Puppet::Util::IPCidr.new('2001:db8:85a3:0:0:8a2e:370:7334') } + subject { @ipaddr } + specify { subject.cidr.should == '2001:db8:85a3::8a2e:370:7334/128' } + specify { subject.prefixlen.should == 128 } + specify { subject.netmask.should == 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' } + end + + describe 'single ipv6 addr with cidr' do + before { @ipaddr = Puppet::Util::IPCidr.new('2001:db8:85a3:0:0:8a2e:370:7334/128') } + subject { @ipaddr } + specify { subject.cidr.should == '2001:db8:85a3::8a2e:370:7334/128' } + specify { subject.prefixlen.should == 128 } + specify { subject.netmask.should == 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' } + end + + describe 'ipv6 addr range with cidr' do + before { @ipaddr = Puppet::Util::IPCidr.new('2001:db8:1234::/48') } + subject { @ipaddr } + specify { subject.cidr.should == '2001:db8:1234::/48' } + specify { subject.prefixlen.should == 48 } + specify { subject.netmask.should == 'ffff:ffff:ffff:0000:0000:0000:0000:0000' } + end + + describe 'ipv6 open range with cidr' do + before { @ipaddr = Puppet::Util::IPCidr.new('::/0') } + subject { @ipaddr } + specify { subject.cidr.should == '::/0' } + specify { subject.prefixlen.should == 0 } + specify { subject.netmask.should == '0000:0000:0000:0000:0000:0000:0000:0000' } + end +end diff --git a/galera b/galera deleted file mode 160000 index e35922bbb..000000000 --- a/galera +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e35922bbb31ef2e6a86c7973cbafea96a8b160af diff --git a/galera/.fixtures.yml b/galera/.fixtures.yml new file mode 100644 index 000000000..67f800fa3 --- /dev/null +++ b/galera/.fixtures.yml @@ -0,0 +1,7 @@ +fixtures: + repositories: + "stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib" + "xinetd": "git://github.com/packstack/puppetlabs-xinetd" + "mysql": "git://github.com/packstack/puppetlabs-mysql" + symlinks: + "galera": "#{source_dir}" diff --git a/galera/.gitignore b/galera/.gitignore new file mode 100644 index 000000000..05823c3ae --- /dev/null +++ b/galera/.gitignore @@ -0,0 +1,5 @@ +Gemfile.lock +.vagrant +vendor +spec/fixtures/modules +spec/fixtures/manifests/site.pp diff --git a/galera/Gemfile b/galera/Gemfile new file mode 100644 index 000000000..fd321c0f3 --- /dev/null +++ b/galera/Gemfile @@ -0,0 +1,20 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'rake', :require => false + gem 'rspec-puppet', :require => false + gem 'puppetlabs_spec_helper', :require => false + gem 'rspec-system', :require => false + gem 'rspec-system-puppet', :require => false + gem 'rspec-system-serverspec', :require => false + gem 'serverspec', :require => false + gem 'puppet-lint', :require => false +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/galera/LICENSE b/galera/LICENSE new file mode 100644 index 000000000..8d968b6cb --- /dev/null +++ b/galera/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/galera/Modulefile b/galera/Modulefile new file mode 100644 index 000000000..57a50db3c --- /dev/null +++ b/galera/Modulefile @@ -0,0 +1,11 @@ +name 'puppet-galera' +version '0.0.2' +source 'https://github.com/rohara/puppet-galera' +author 'Ryan O'Hara' +license 'Apache License 2.0' +summary 'Install/configure MariaDB with Galera' +description 'Install/configure MariaDB with Galera' +project_page 'https://github.com/rohara/puppet-galera' + +dependency 'puppetlabs/mysql', '>= 0.5.0' +dependency 'puppetlabs/xinetd', '>= 1.2.0' diff --git a/galera/README.md b/galera/README.md new file mode 100644 index 000000000..c07953245 --- /dev/null +++ b/galera/README.md @@ -0,0 +1,50 @@ +# Galera module + +This is a module for installing and confiuring galera. + +It depends on the mysql module from puppetlabs as well as xinetd. + +## Usage + +### galera::server + + Used to deploy and manage a MariaDB Galera server cluster. Installs + mariadb-galera-server and galera packages, configures galera.cnf and + starts mysqld service: + + class { 'galera::server': + config_hash => { + bind_address => '0.0.0.0', + default_engine => 'InnoDB', + root_password => 'root_pass', + }, + wsrep_cluster_name => 'galera_cluster', + wsrep_sst_method => 'rsync' + wsrep_sst_username => 'ChangeMe', + wsrep_sst_password => 'ChangeMe', + } + +### galera::monitor + + Used to monitor a MariaDB Galera cluster server. The class is meant + to be used in a server load-balancer environment. + + class {'galera::monitor': + monitor_username => 'mon_user', + monitor_password => 'mon_pass' + } + + Here is a sample 3-node HAProxy Configuration: + + listen galera 192.168.220.40:3306 + balance leastconn + mode tcp + option tcpka + option httpchk + server control01 192.168.220.41:3306 check port 9200 inter 2000 rise 2 fall 5 + server control02 192.168.220.42:3306 check port 9200 inter 2000 rise 2 fall 5 + server control03 192.168.220.43:3306 check port 9200 inter 2000 rise 2 fall 5 + +## Authors + +Daneyon Hansen, Ryan O'Hara diff --git a/galera/Rakefile b/galera/Rakefile new file mode 100644 index 000000000..bb60173e5 --- /dev/null +++ b/galera/Rakefile @@ -0,0 +1,2 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'rspec-system/rake_task' diff --git a/galera/lib/puppet/parser/functions/wsrep_options.rb b/galera/lib/puppet/parser/functions/wsrep_options.rb new file mode 100644 index 000000000..f09049026 --- /dev/null +++ b/galera/lib/puppet/parser/functions/wsrep_options.rb @@ -0,0 +1,9 @@ +module Puppet::Parser::Functions + newfunction(:wsrep_options, :type => :rvalue) do |args| + opts = args[0] + opts.delete_if {|key, val| val.equal? :undef} + opts.sort.map do |k,v| + String(k) + "=" + String(v) + end + end +end diff --git a/galera/manifests/monitor.pp b/galera/manifests/monitor.pp new file mode 100644 index 000000000..75deb4d01 --- /dev/null +++ b/galera/manifests/monitor.pp @@ -0,0 +1,65 @@ +# Class galera::monitor +# +# Parameters: +# [*mysql_username*] - Username of the service account used for the clustercheck script. +# [*mysql_password*] - Password of the service account used for the clustercheck script. +# [*mysql_host*] - Hostname/IP address of mysql server to monitor. Defaults to 127.0.0.1. +# [*mysql_port] - Port used by mysql service. Defaults to 3306. +# [*monitor_port*] - Port used by galera monitor service. Defaults to 9200. +# [*monitor_script*] - Full path to monitor script. Defaults to '/usr/bin/clustercheck'. +# [*enabled*] - Enable/Disable galera monitor xinetd::service. Defaults to true. +# +# Actions: +# +# Requires: +# +# Sample usage: +# class { 'galera::monitor': +# mysql_username => 'mon_user', +# mysql_password => 'mon_pass' +# } +# +class galera::monitor ( + $mysql_username = 'monitor_user', + $mysql_password = 'monitor_pass', + $mysql_host = '127.0.0.1', + $mysql_port = '3306', + $monitor_port = '9200', + $monitor_script = '/usr/bin/clustercheck', + $enabled = true, +) { + + Class['galera::server'] -> Class['galera::monitor'] + + if $enabled { + $monitor_disable = 'no' + } else { + $monitor_disable = 'yes' + } + + file { '/etc/sysconfig/clustercheck': + mode => '0640', + content => template("galera/clustercheck.erb"), + owner => 'root', + group => 'root', + } + + xinetd::service { 'galera-monitor': + disable => $monitor_disable, + port => $monitor_port, + server => $monitor_script, + flags => 'REUSE', + per_source => 'UNLIMITED', + service_type => 'UNLISTED', + log_on_success => '', + log_on_success_operator => '=', + log_on_failure => 'HOST', + log_on_failure_operator => '=', + } + + database_user { "${mysql_username}@${mysql_host}": + ensure => present, + password_hash => mysql_password($mysql_password), + require => [File['/root/.my.cnf'],Service['galera']], + } +} diff --git a/galera/manifests/server.pp b/galera/manifests/server.pp new file mode 100644 index 000000000..4d328543e --- /dev/null +++ b/galera/manifests/server.pp @@ -0,0 +1,100 @@ +# Class: galera::server +# +# manages the installation of the galera server. +# manages the package, service, galera.cnf +# +# Parameters: +# [*config_hash*] - Hash of config parameters that need to be set. +# [*bootstrap*] - Defaults to false, boolean to set cluster boostrap. +# [*package_name*] - The name of the galera package. +# [*package_ensure*] - Ensure state for package. Can be specified as version. +# [*service_name*] - The name of the galera service. +# [*service_enable*] - Defaults to true, boolean to set service enable. +# [*service_ensure*] - Defaults to running, needed to set root password. +# [*service_provider*] - What service provider to use. +# [*wsrep_bind_address*] - Address to bind galera service. +# [*wsrep_provider*] - Full path to wsrep provider library or 'none'. +# [*wsrep_cluster_name*] - Logical cluster name. Should be the same for all nodes. +# [*wsrep_cluster_members*] - List of cluster members, IP addresses or hostnames. +# [*wsrep_sst_method*] - State snapshot transfer method. +# [*wsrep_sst_username*] - Username used by the wsrep_sst_auth authentication string. +# [*wsrep_sst_password*] - Password used by the wsrep_sst_auth authentication string. +# [*wsrep_ssl*] - Boolean to disable SSL even if certificate and key are configured. +# [*wsrep_ssl_key*] - Private key for the certificate above, unencrypted, in PEM format. +# [*wsrep_ssl_cert*] - Certificate file in PEM format. +# +# Actions: +# +# Requires: +# +# Sample Usage: +# class { 'galera::server': +# config_hash => { +# bind_address => '0.0.0.0', +# default_engine => 'InnoDB', +# root_password => 'root_pass', +# }, +# wsrep_cluster_name => 'galera_cluster', +# wsrep_sst_method => 'rsync' +# wsrep_sst_username => 'ChangeMe', +# wsrep_sst_password => 'ChangeMe', +# } +# +class galera::server ( + $config_hash = {}, + $bootstrap = false, + $debug = false, + $package_name = 'mariadb-galera-server', + $package_ensure = 'present', + $service_name = $mysql::params::service_name, + $service_enable = true, + $service_ensure = 'running', + $service_provider = $mysql::params::service_provider, + $wsrep_bind_address = '0.0.0.0', + $wsrep_provider = '/usr/lib64/galera/libgalera_smm.so', + $wsrep_cluster_name = 'galera_cluster', + $wsrep_cluster_members = [ $::ipaddress ], + $wsrep_sst_method = 'rsync', + $wsrep_sst_username = 'root', + $wsrep_sst_password = undef, + $wsrep_ssl = false, + $wsrep_ssl_key = undef, + $wsrep_ssl_cert = undef, +) inherits mysql { + + $config_class = { 'mysql::config' => $config_hash } + + create_resources( 'class', $config_class ) + + package { 'galera': + name => $package_name, + ensure => $package_ensure, + } + + $wsrep_provider_options = wsrep_options({ + 'socket.ssl' => $wsrep_ssl, + 'socket.ssl_key' => $wsrep_ssl_key, + 'socket.ssl_cert' => $wsrep_ssl_cert, + }) + + $wsrep_debug = bool2num($debug) + + file { '/etc/my.cnf.d/galera.cnf': + ensure => present, + mode => '0644', + owner => 'root', + group => 'root', + content => template('galera/wsrep.cnf.erb'), + notify => Service['galera'], + } + + Service['galera'] -> Exec<| title == 'set_mysql_rootpw' |> + + service { 'galera': + name => $service_name, + enable => $service_enable, + ensure => $service_ensure, + require => Package['galera'], + provider => $service_provider, + } +} diff --git a/galera/spec/classes/galera_monitor_spec.rb b/galera/spec/classes/galera_monitor_spec.rb new file mode 100644 index 000000000..47e5a1d36 --- /dev/null +++ b/galera/spec/classes/galera_monitor_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' +describe 'galera::monitor' do + let :facts do + { :osfamily => 'RedHat' } + end + let :pre_condition do + "include 'galera::server'" + end + let :params do + { + :monitor_username => 'monitor_user', + :monitor_password => 'monitor_pass', + :monitor_hostname => '127.0.0.1', + :mysql_port => '3306', + :mysql_path => '/usr/bin/mysql', + :script_dir => '/usr/local/bin', + :enabled => true + } + end + + it { should contain_service('xinetd').with( + :ensure => 'running', + :enable => 'true' + )} + + it { should contain_file('/usr/local/bin/galera_chk')} + it { should contain_database_user("monitor_user@127.0.0.1")} + +end diff --git a/galera/spec/classes/galera_server_spec.rb b/galera/spec/classes/galera_server_spec.rb new file mode 100644 index 000000000..6abff142b --- /dev/null +++ b/galera/spec/classes/galera_server_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +describe 'galera::server' do + let :facts do + { :osfamily => 'RedHat' } + end + let :params do + { + :package_name => 'mariadb-galera-server', + :package_ensure => 'present', + :service_name => 'mariadb', + :service_enable => false, + :service_ensure => 'running', + :wsrep_bind_address => '0.0.0.0', + :wsrep_provider => '/usr/lib64/galera/libgalera_smm.so', + :wsrep_cluster_name => 'galera_cluster', + :wsrep_cluster_address => 'gcomm://', + :wsrep_sst_method => 'rsync', + :wsrep_sst_username => 'wsrep_user', + :wsrep_sst_password => 'wsrep_pass', + } + end + + it { should contain_package('galera')} + it { should contain_file('/etc/my.cnf.d/galera.cnf')} + it { should contain_service('galera')} + +end diff --git a/galera/spec/spec_helper.rb b/galera/spec/spec_helper.rb new file mode 100644 index 000000000..2c6f56649 --- /dev/null +++ b/galera/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/galera/templates/clustercheck.erb b/galera/templates/clustercheck.erb new file mode 100644 index 000000000..3abac52c5 --- /dev/null +++ b/galera/templates/clustercheck.erb @@ -0,0 +1,7 @@ +# This file is being maintained by Puppet. +# DO NOT EDIT + +MYSQL_USERNAME="<%= @mysql_username %>" +MYSQL_PASSWORD="<%= @mysql_password %>" +MYSQL_HOST="<%= @mysql_host %>" +MYSQL_PORT="<%= @mysql_port %>" diff --git a/galera/templates/wsrep.cnf.erb b/galera/templates/wsrep.cnf.erb new file mode 100644 index 000000000..8057d8f37 --- /dev/null +++ b/galera/templates/wsrep.cnf.erb @@ -0,0 +1,133 @@ +# This file contains wsrep-related mysqld options. It should be included +# in the main MySQL configuration file. +# +# Options that need to be customized: +# - wsrep_provider +# - wsrep_cluster_address +# - wsrep_sst_auth +# The rest of defaults should work out of the box. + +## +## mysqld options _MANDATORY_ for correct opration of the cluster +## +[mysqld] + +# (This must be substituted by wsrep_format) +binlog_format=ROW + +# Currently only InnoDB storage engine is supported +default-storage-engine=innodb + +# to avoid issues with 'bulk mode inserts' using autoinc +innodb_autoinc_lock_mode=2 + +# This is a must for paralell applying +innodb_locks_unsafe_for_binlog=1 + +# Query Cache is not supported with wsrep +query_cache_size=0 +query_cache_type=0 + +# Override bind-address +# In some systems bind-address defaults to 127.0.0.1, and with mysqldump SST +# it will have (most likely) disastrous consequences on donor node +bind-address=<%= @wsrep_bind_address %> + +## +## WSREP options +## + +# Full path to wsrep provider library or 'none' +wsrep_provider=<%= @wsrep_provider %> + +# Provider specific configuration options +wsrep_provider_options="<%= @wsrep_provider_options.join '; ' %>" + +# Logical cluster name. Should be the same for all nodes. +wsrep_cluster_name="<%= @wsrep_cluster_name %>" + +# Group communication system handle +<% if @bootstrap -%> +wsrep_cluster_address="gcomm://" +<% else -%> +wsrep_cluster_address="gcomm://<%= @wsrep_cluster_members.join ',' %>" +<% end -%> + +# Human-readable node name (non-unique). Hostname by default. +#wsrep_node_name= + +# Base replication [:port] of the node. +# The values supplied will be used as defaults for state transfer receiving, +# listening ports and so on. Default: address of the first network interface. +#wsrep_node_address= + +# Address for incoming client connections. Autodetect by default. +#wsrep_node_incoming_address= + +# How many threads will process writesets from other nodes +wsrep_slave_threads=1 + +# DBUG options for wsrep provider +#wsrep_dbug_option + +# Generate fake primary keys for non-PK tables (required for multi-master +# and parallel applying operation) +wsrep_certify_nonPK=1 + +# Maximum number of rows in write set +wsrep_max_ws_rows=131072 + +# Maximum size of write set +wsrep_max_ws_size=1073741824 + +# to enable debug level logging, set this to 1 +wsrep_debug=<%= @wsrep_debug %> + +# convert locking sessions into transactions +wsrep_convert_LOCK_to_trx=0 + +# how many times to retry deadlocked autocommits +wsrep_retry_autocommit=1 + +# change auto_increment_increment and auto_increment_offset automatically +wsrep_auto_increment_control=1 + +# retry autoinc insert, which failed for duplicate key error +wsrep_drupal_282555_workaround=0 + +# enable "strictly synchronous" semantics for read operations +wsrep_causal_reads=0 + +# Command to call when node status or cluster membership changes. +# Will be passed all or some of the following options: +# --status - new status of this node +# --uuid - UUID of the cluster +# --primary - whether the component is primary or not ("yes"/"no") +# --members - comma-separated list of members +# --index - index of this node in the list +wsrep_notify_cmd= + +## +## WSREP State Transfer options +## + +# State Snapshot Transfer method +wsrep_sst_method=<%= @wsrep_sst_method %> + +# Address which donor should send State Snapshot to. +# Should be the address of THIS node. DON'T SET IT TO DONOR ADDRESS!!! +# (SST method dependent. Defaults to the first IP of the first interface) +#wsrep_sst_receive_address= + +# SST authentication string. This will be used to send SST to joining nodes. +# Depends on SST method. For mysqldump method it is root: +wsrep_sst_auth=<%= @wsrep_sst_username %>:<%= @wsrep_sst_password %> + +# Desired SST donor name. +#wsrep_sst_donor= + +# Reject client queries when donating SST (false) +#wsrep_sst_donor_rejects_queries=0 + +# Protocol version to use +# wsrep_protocol_version= diff --git a/glance b/glance deleted file mode 160000 index f377c0229..000000000 --- a/glance +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f377c0229c006b02f43a14be4979553e983cb98e diff --git a/glance/.fixtures.yml b/glance/.fixtures.yml new file mode 100644 index 000000000..70fa5d661 --- /dev/null +++ b/glance/.fixtures.yml @@ -0,0 +1,18 @@ +fixtures: + repositories: + "apt": "git://github.com/puppetlabs/puppetlabs-apt.git" + "keystone": "git://github.com/stackforge/puppet-keystone.git" + "mysql": + repo: "git://github.com/puppetlabs/puppetlabs-mysql.git" + ref: 'origin/2.2.x' + 'openstacklib': 'git://github.com/stackforge/puppet-openstacklib.git' + "stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib.git" + "rabbitmq": + repo: "git://github.com/puppetlabs/puppetlabs-rabbitmq" + ref: 'origin/2.x' + 'inifile': 'git://github.com/puppetlabs/puppetlabs-inifile' + "postgresql": + repo: "git://github.com/puppetlabs/puppet-postgresql.git" + ref: "2.5.0" + symlinks: + "glance": "#{source_dir}" diff --git a/glance/.gitignore b/glance/.gitignore new file mode 100644 index 000000000..bde49f4db --- /dev/null +++ b/glance/.gitignore @@ -0,0 +1,5 @@ +spec/fixtures/modules/* +spec/fixtures/manifests/* +*swp +pkg +Gemfile.lock diff --git a/glance/.gitreview b/glance/.gitreview new file mode 100644 index 000000000..074651a11 --- /dev/null +++ b/glance/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=stackforge/puppet-glance.git diff --git a/glance/Gemfile b/glance/Gemfile new file mode 100644 index 000000000..d965fa900 --- /dev/null +++ b/glance/Gemfile @@ -0,0 +1,18 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'puppetlabs_spec_helper', :require => false + gem 'puppet-lint', '~> 0.3.2' + gem 'rake', '10.1.1' + gem 'rspec', '< 2.99' + gem 'json' + gem 'webmock' +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/glance/LICENSE b/glance/LICENSE new file mode 100644 index 000000000..8d968b6cb --- /dev/null +++ b/glance/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/glance/Modulefile b/glance/Modulefile new file mode 100644 index 000000000..cd5341d4d --- /dev/null +++ b/glance/Modulefile @@ -0,0 +1,13 @@ +name 'puppetlabs-glance' +version '4.0.0' +author 'Puppet Labs and StackForge Contributors' +license 'Apache License 2.0' +summary 'Puppet module for OpenStack Glance' +description 'Installs and configures OpenStack Glance (Image Service).' +project_page 'https://launchpad.net/puppet-glance' +source 'https://github.com/stackforge/puppet-glance' + +dependency 'puppetlabs/inifile', '>=1.0.0 <2.0.0' +dependency 'puppetlabs/keystone', '>=4.0.0 <5.0.0' +dependency 'puppetlabs/stdlib', '>=4.0.0 <5.0.0' +dependency 'stackforge/openstacklib', '>=5.0.0' diff --git a/glance/README.md b/glance/README.md new file mode 100644 index 000000000..b79a172b7 --- /dev/null +++ b/glance/README.md @@ -0,0 +1,189 @@ +glance +======= + +4.0.0 - 2014.1.0 - Icehouse + +#### Table of Contents + +1. [Overview - What is the glance module?](#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with glance](#setup) +4. [Implementation - An under-the-hood peek at what the module is doing](#implementation) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) +7. [Contributors - Those with commits](#contributors) +8. [Release Notes - Notes on the most recent updates to the module](#release-notes) + +Overview +-------- + +The glance module is a part of [Stackforge](https://github.com/stackfoge), an effort by the Openstack infrastructure team to provide continuous integration testing and code review for Openstack and Openstack community projects not part of the core software. The module its self is used to flexibly configure and manage the image service for Openstack. + +Module Description +------------------ + +The glance module is a thorough attempt to make Puppet capable of managing the entirety of glance. This includes manifests to provision such things as keystone endpoints, RPC configurations specific to glance, and database connections. Types are shipped as part of the glance module to assist in manipulation of configuration files. + +This module is tested in combination with other modules needed to build and leverage an entire Openstack software stack. These modules can be found, all pulled together in the [openstack module](https://github.com/stackfoge/puppet-openstack). + +Setup +----- + +**What the glance module affects** + +* glance, the image service for Openstack. + +### Installing glance + + example% puppet module install puppetlabs/glance + +### Beginning with glance + +To utilize the glance module's functionality you will need to declare multiple resources. The following is a modified excerpt from the [openstack module](https://github.com/stackfoge/puppet-openstack). This is not an exhaustive list of all the components needed, we recommend you consult and understand the [openstack module](https://github.com/stackforge/puppet-openstack) and the [core openstack](http://docs.openstack.org) documentation. + +**Define a glance node** + +```puppet +class { 'glance::api': + verbose => true, + keystone_tenant => 'services', + keystone_user => 'glance', + keystone_password => '12345', + sql_connection => 'mysql://glance:12345@127.0.0.1/glance', +} + +class { 'glance::registry': + verbose => true, + keystone_tenant => 'services', + keystone_user => 'glance', + keystone_password => '12345', + sql_connection => 'mysql://glance:12345@127.0.0.1/glance', +} + +class { 'glance::backend::file': } +``` + +**Setup postgres node glance** + +```puppet +class { 'glance::db::postgresql': + password => '12345', +} +``` + +**Setup mysql node for glance** + +```puppet +class { 'glance::db::mysql': + password => '12345', + allowed_hosts => '%', +} +``` + +**Setup up keystone endpoints for glance on keystone node** + +```puppet +class { 'glance::keystone::auth': + password => '12345' + email => 'glance@example.com', + public_address => '172.17.0.3', + admin_address => '172.17.0.3', + internal_address => '172.17.1.3', + region => 'example-west-1', +} +``` + +**Setup up notifications for multiple RabbitMQ nodes** + +```puppet +class { 'glance::notify::rabbitmq': + rabbit_password => 'pass', + rabbit_userid => 'guest', + rabbit_hosts => [ + 'localhost:5672', 'remotehost:5672' + ], + rabbit_use_ssl => false, +} +``` + +Implementation +-------------- + +### glance + +glance is a combination of Puppet manifest and ruby code to deliver configuration and extra functionality through types and providers. + +Limitations +------------ + +* Only supports configuring the file, swift and rbd storage backends. + +Development +----------- + +Developer documentation for the entire puppet-openstack project. + +* https://wiki.openstack.org/wiki/Puppet-openstack#Developer_documentation + +Contributors +------------ + +* https://github.com/stackforge/puppet-glance/graphs/contributors + +Release Notes +------------- + +**4.0.0** + +* Stable Icehouse release. +* Added glance::config to handle additional custom options. +* Added known_stores option for glance::api. +* Added copy-on-write cloning of images to volumes. +* Added support for puppetlabs-mysql 2.2 and greater. +* Added support for python-glanceclient v2 API update. +* Removed deprecated notifier_stratgy parameter. +* Deprecated show_image_direct_url in glance::rbd. + +**3.1.0** + +* Added availability to configure show_image_direct_url. +* Removed Keystone client warnings. +* Added support for https authentication endpoints. +* Enabled ssl configuration for glance-registry. +* Explicitly sets default notifier strategy. + +**3.0.0** + +* Major release for OpenStack Havana. +* Fixed bug to ensure keystone endpoint is set before service starts. +* Added Cinder backend to image storage. +* Fixed qpid_hostname bug. + +**2.2.0** + +* Added syslog support. +* Added support for iso disk format. +* Fixed bug to allow support for rdb options in glance-api.conf. +* Fixed bug for rabbitmq options in notify::rabbitmq. +* Removed non-implemented glance::scrubber class. +* Various lint and bug fixes. + +**2.1.0** + +* Added glance-cache-cleaner and glance-cache-pruner. +* Added ceph/rdb support. +* Added retry for glance provider to account for service startup time. +* Added support for both file and swift backends. +* Fixed allowed_hosts/database access bug. +* Fixed glance_image type example. +* Removed unnecessary mysql::server dependency. +* Removed --silent-upload option. +* Removed glance-manage version_control. +* Pinned rabbit and mysql module versions. +* Various lint and bug fixes. + +**2.0.0** + +* Upstream is now part of stackfoge. +* Added postgresql support. +* Various cleanups and bug fixes. diff --git a/glance/Rakefile b/glance/Rakefile new file mode 100644 index 000000000..4c2b2ed07 --- /dev/null +++ b/glance/Rakefile @@ -0,0 +1,6 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings = true +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_parameter_defaults') diff --git a/glance/ext/glance.rb b/glance/ext/glance.rb new file mode 100644 index 000000000..6510397c0 --- /dev/null +++ b/glance/ext/glance.rb @@ -0,0 +1,86 @@ +#!/usr/bin/env ruby +# +# test that we can upload and download files +# +require 'open3' +require 'fileutils' + +keystone_public = '127.0.0.1' +image_dir='/tmp/images' + +ENV['OS_USERNAME']='admin' +ENV['OS_TENANT_NAME']='admin' +ENV['OS_PASSWORD']='ChangeMe' +ENV['OS_AUTH_URL']='http://127.0.0.1:5000/v2.0/' +ENV['OS_REGION_NAME']='RegionOne' + +FileUtils.mkdir_p(image_dir) +Dir.chdir(image_dir) do |dir| + + kernel_id = nil + initrd_id = nil + + remote_image_url='http://smoser.brickies.net/ubuntu/ttylinux-uec/ttylinux-uec-amd64-12.1_2.6.35-22_1.tar.gz; tar -zxvf ttylinux-uec-amd64-12.1_2.6.35-22_1.tar.gz' + + wget_command = "wget #{remote_image_url}" + + Open3.popen3(wget_command) do |stdin, stdout, stderr| + puts "wget stdout: #{stdout.read}" + puts "wget stderr: #{stderr.read}" + end + + add_kernel='disk_format=aki container_format=aki < ttylinux-uec-amd64-12.1_2.6.35-22_1-vmlinuz' + kernel_name='tty-linux-kernel' + kernel_format='aki' + + add_kernel_command="glance add name='#{kernel_name}' disk_format='#{kernel_format}' container_format=#{kernel_format} < ttylinux-uec-amd64-12.1_2.6.35-22_1-vmlinuz" + + Open3.popen3(add_kernel_command) do |stdin, stdout, stderr| + stdout = stdout.read.split("\n") + stdout.each do |line| + if line =~ /Added new image with ID: (\w+)/ + kernel_id = $1 + end + end + puts stderr.read + puts stdout + end + + raise(Exception, 'Did not add kernel successfully') unless kernel_id + + initrd_id = nil + add_initrd_command="glance add name='tty-linux-ramdisk' disk_format=ari container_format=ari < ttylinux-uec-amd64-12.1_2.6.35-22_1-loader" + + Open3.popen3(add_initrd_command) do |stdin, stdout, stderr| + stdout = stdout.read.split("\n") + stdout.each do |line| + if line =~ /Added new image with ID: (\w+)/ + initrd_id = $1 + end + end + puts stderr.read + puts stdout + end + + raise(Exception, 'Did not add initrd successfully') unless initrd_id + + add_image_command="glance add name='tty-linux' disk_format=ami container_format=ami kernel_id=#{kernel_id} ramdisk_id=#{initrd_id} < ttylinux-uec-amd64-12.1_2.6.35-22_1.img" + + Open3.popen3(add_image_command) do |stdin, stdout, stderr| + stdout = stdout.read.split("\n") + stdout.each do |line| + if line =~ /Added new image with ID: (\w+)/ + kernel_id = $1 + end + end + puts stderr.read + puts stdout + end + + get_index='glance index' + + Open3.popen3(get_index) do |stdin, stdout, stderr| + puts stdout.read + puts stderr.read + end +end diff --git a/glance/ext/glance.sh b/glance/ext/glance.sh new file mode 100755 index 000000000..174749981 --- /dev/null +++ b/glance/ext/glance.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# +# assumes that resonable credentials have been stored at +# /root/auth +source /root/auth +wget http://uec-images.ubuntu.com/releases/11.10/release/ubuntu-11.10-server-cloudimg-amd64-disk1.img +glance add name="Ubuntu 11.10 cloudimg amd64" is_public=true container_format=ovf disk_format=qcow2 < ubuntu-11.10-server-cloudimg-amd64-disk1.img +glance index diff --git a/glance/lib/puppet/provider/glance.rb b/glance/lib/puppet/provider/glance.rb new file mode 100644 index 000000000..d0a7bb66b --- /dev/null +++ b/glance/lib/puppet/provider/glance.rb @@ -0,0 +1,186 @@ +# Since there's only one glance type for now, +# this probably could have all gone in the provider file. +# But maybe this is good long-term. +require 'puppet/util/inifile' +class Puppet::Provider::Glance < Puppet::Provider + + def self.glance_credentials + @glance_credentials ||= get_glance_credentials + end + + def self.get_glance_credentials + if glance_file and glance_file['keystone_authtoken'] and + glance_file['keystone_authtoken']['auth_host'] and + glance_file['keystone_authtoken']['auth_port'] and + glance_file['keystone_authtoken']['auth_protocol'] and + glance_file['keystone_authtoken']['admin_tenant_name'] and + glance_file['keystone_authtoken']['admin_user'] and + glance_file['keystone_authtoken']['admin_password'] + + g = {} + g['auth_host'] = glance_file['keystone_authtoken']['auth_host'].strip + g['auth_port'] = glance_file['keystone_authtoken']['auth_port'].strip + g['auth_protocol'] = glance_file['keystone_authtoken']['auth_protocol'].strip + g['admin_tenant_name'] = glance_file['keystone_authtoken']['admin_tenant_name'].strip + g['admin_user'] = glance_file['keystone_authtoken']['admin_user'].strip + g['admin_password'] = glance_file['keystone_authtoken']['admin_password'].strip + + # auth_admin_prefix not required to be set. + g['auth_admin_prefix'] = (glance_file['keystone_authtoken']['auth_admin_prefix'] || '').strip + + return g + else + raise(Puppet::Error, 'File: /etc/glance/glance-api.conf does not contain all required sections.') + end + end + + def glance_credentials + self.class.glance_credentials + end + + def self.auth_endpoint + @auth_endpoint ||= get_auth_endpoint + end + + def self.get_auth_endpoint + g = glance_credentials + "#{g['auth_protocol']}://#{g['auth_host']}:#{g['auth_port']}#{g['auth_admin_prefix']}/v2.0/" + end + + def self.glance_file + return @glance_file if @glance_file + @glance_file = Puppet::Util::IniConfig::File.new + @glance_file.read('/etc/glance/glance-api.conf') + @glance_file + end + + def self.glance_hash + @glance_hash ||= build_glance_hash + end + + def self.reset + @glance_hash = nil + @glance_file = nil + @glance_credentials = nil + @auth_endpoint = nil + end + + def glance_hash + self.class.glance_hash + end + + def self.auth_glance(*args) + begin + g = glance_credentials + remove_warnings(glance('--os-tenant-name', g['admin_tenant_name'], '--os-username', g['admin_user'], '--os-password', g['admin_password'], '--os-auth-url', auth_endpoint, args)) + rescue Exception => e + if (e.message =~ /\[Errno 111\] Connection refused/) or (e.message =~ /\(HTTP 400\)/) or (e.message =~ /HTTP Unable to establish connection/) + sleep 10 + remove_warnings(glance('--os-tenant-name', g['admin_tenant_name'], '--os-username', g['admin_user'], '--os-password', g['admin_password'], '--os-auth-url', auth_endpoint, args)) + else + raise(e) + end + end + end + + def auth_glance(*args) + self.class.auth_glance(args) + end + + def self.auth_glance_stdin(*args) + begin + g = glance_credentials + command = "glance --os-tenant-name #{g['admin_tenant_name']} --os-username #{g['admin_user']} --os-password #{g['admin_password']} --os-auth-url #{auth_endpoint} #{args.join(' ')}" + + # This is a horrible, horrible hack + # Redirect stderr to stdout in order to report errors + # Ignore good output + err = `#{command} 3>&1 1>/dev/null 2>&3` + if $? != 0 + raise(Puppet::Error, err) + end + end + end + + def auth_glance_stdin(*args) + self.class.auth_glance_stdin(args) + end + + private + def self.list_glance_images + ids = [] + (auth_glance('image-list').split("\n")[3..-2] || []).collect do |line| + ids << line.split('|')[1].strip() + end + return ids + end + + def self.get_glance_image_attr(id, attr) + (auth_glance('image-show', id).split("\n") || []).collect do |line| + if line =~ /^#{attr}:/ + return line.split(': ')[1..-1] + end + end + end + + def self.get_glance_image_attrs(id) + attrs = {} + (auth_glance('image-show', id).split("\n")[3..-2] || []).collect do |line| + attrs[line.split('|')[1].strip()] = line.split('|')[2].strip() + end + return attrs + end + + def parse_table(table) + # parse the table into an array of maps with a simplistic state machine + found_header = false + parsed_header = false + keys = nil + results = [] + table.split("\n").collect do |line| + # look for the header + if not found_header + if line =~ /^\+[-|+]+\+$/ + found_header = true + nil + end + # look for the key names in the table header + elsif not parsed_header + if line =~ /^(\|\s*[:alpha:]\s*)|$/ + keys = line.split('|').map(&:strip) + parsed_header = true + end + # parse the values in the rest of the table + elsif line =~ /^|.*|$/ + values = line.split('|').map(&:strip) + result = Hash[keys.zip values] + results << result + end + end + results + end + + # Remove warning from the output. This is a temporary hack until + # things will be refactored to use the REST API + def self.remove_warnings(results) + found_header = false + in_warning = false + results.split("\n").collect do |line| + unless found_header + if line =~ /^\+[-\+]+\+$/ # Matches upper and lower box borders + in_warning = false + found_header = true + line + elsif line =~ /^WARNING/ or line =~ /UserWarning/ or in_warning + # warnings can be multi line, we have to skip all of them + in_warning = true + nil + else + line + end + else + line + end + end.compact.join("\n") + end +end diff --git a/glance/lib/puppet/provider/glance_api_config/ini_setting.rb b/glance/lib/puppet/provider/glance_api_config/ini_setting.rb new file mode 100644 index 000000000..4323ae8cd --- /dev/null +++ b/glance/lib/puppet/provider/glance_api_config/ini_setting.rb @@ -0,0 +1,27 @@ +Puppet::Type.type(:glance_api_config).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/glance/glance-api.conf' + end + + # this needs to be removed. This has been replaced with the class method + def file_path + self.class.file_path + end + +end diff --git a/glance/lib/puppet/provider/glance_api_paste_ini/ini_setting.rb b/glance/lib/puppet/provider/glance_api_paste_ini/ini_setting.rb new file mode 100644 index 000000000..d624d64bc --- /dev/null +++ b/glance/lib/puppet/provider/glance_api_paste_ini/ini_setting.rb @@ -0,0 +1,27 @@ +Puppet::Type.type(:glance_api_paste_ini).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/glance/glance-api-paste.ini' + end + + # added for backwards compatibility with older versions of inifile + def file_path + self.class.file_path + end + +end diff --git a/glance/lib/puppet/provider/glance_cache_config/ini_setting.rb b/glance/lib/puppet/provider/glance_cache_config/ini_setting.rb new file mode 100644 index 000000000..579dfb469 --- /dev/null +++ b/glance/lib/puppet/provider/glance_cache_config/ini_setting.rb @@ -0,0 +1,27 @@ +Puppet::Type.type(:glance_cache_config).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/glance/glance-cache.conf' + end + + # added for backwards compatibility with older versions of inifile + def file_path + self.class.file_path + end + +end diff --git a/glance/lib/puppet/provider/glance_image/glance.rb b/glance/lib/puppet/provider/glance_image/glance.rb new file mode 100644 index 000000000..ab9d27b87 --- /dev/null +++ b/glance/lib/puppet/provider/glance_image/glance.rb @@ -0,0 +1,121 @@ +# Load the Glance provider library to help +require File.join(File.dirname(__FILE__), '..','..','..', 'puppet/provider/glance') + +Puppet::Type.type(:glance_image).provide( + :glance, + :parent => Puppet::Provider::Glance +) do + desc <<-EOT + Glance provider to manage glance_image type. + + Assumes that the glance-api service is on the same host and is working. + EOT + + commands :glance => 'glance' + + mk_resource_methods + + def self.instances + list_glance_images.collect do |image| + attrs = get_glance_image_attrs(image) + new( + :ensure => :present, + :name => attrs['name'], + :is_public => attrs['is_public'], + :container_format => attrs['container_format'], + :id => attrs['id'], + :disk_format => attrs['disk_format'] + ) + end + end + + def self.prefetch(resources) + images = instances + resources.keys.each do |name| + if provider = images.find{ |pkg| pkg.name == name } + resources[name].provider = provider + end + end + end + + def exists? + @property_hash[:ensure] == :present + end + + def create + stdin = nil + if resource[:source] + # copy_from cannot handle file:// + if resource[:source] =~ /^\// # local file + location = "< #{resource[:source]}" + stdin = true + else + location = "--copy-from=#{resource[:source]}" + end + # location cannot handle file:// + # location does not import, so no sense in doing anything more than this + elsif resource[:location] + location = "--location=#{resource[:location]}" + else + raise(Puppet::Error, "Must specify either source or location") + end + if stdin + result = auth_glance_stdin('image-create', "--name=#{resource[:name]}", "--is-public=#{resource[:is_public]}", "--container-format=#{resource[:container_format]}", "--disk-format=#{resource[:disk_format]}", location) + else + results = auth_glance('image-create', "--name=#{resource[:name]}", "--is-public=#{resource[:is_public]}", "--container-format=#{resource[:container_format]}", "--disk-format=#{resource[:disk_format]}", location) + end + + id = nil + + # Check the old behavior of the python-glanceclient + if results =~ /Added new image with ID: (\S+)/ + id = $1 + else # the new behavior doesn't print the status, so parse the table + results_array = parse_table(results) + results_array.each do |result| + if result["Property"] == "id" + id = result["Value"] + end + end + end + + if id + @property_hash = { + :ensure => :present, + :name => resource[:name], + :is_public => resource[:is_public], + :container_format => resource[:container_format], + :disk_format => resource[:disk_format], + :id => id + } + else + fail("did not get expected message from image creation, got #{results}") + end + end + + def destroy + auth_glance('image-delete', id) + @property_hash[:ensure] = :absent + end + + def location=(value) + auth_glance('image-update', id, "--location=#{value}") + end + + def is_public=(value) + auth_glance('image-update', id, "--is-public=#{value}") + end + + def disk_format=(value) + auth_glance('image-update', id, "--disk-format=#{value}") + end + + def container_format=(value) + auth_glance('image-update', id, "--container-format=#{value}") + end + + def id=(id) + fail('id is read only') + end + +end diff --git a/glance/lib/puppet/provider/glance_registry_config/ini_setting.rb b/glance/lib/puppet/provider/glance_registry_config/ini_setting.rb new file mode 100644 index 000000000..5f843b5e9 --- /dev/null +++ b/glance/lib/puppet/provider/glance_registry_config/ini_setting.rb @@ -0,0 +1,27 @@ +Puppet::Type.type(:glance_registry_config).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/glance/glance-registry.conf' + end + + # added for backwards compatibility with older versions of inifile + def file_path + self.class.file_path + end + +end diff --git a/glance/lib/puppet/provider/glance_registry_paste_ini/ini_setting.rb b/glance/lib/puppet/provider/glance_registry_paste_ini/ini_setting.rb new file mode 100644 index 000000000..36cb7e12c --- /dev/null +++ b/glance/lib/puppet/provider/glance_registry_paste_ini/ini_setting.rb @@ -0,0 +1,27 @@ +Puppet::Type.type(:glance_registry_paste_ini).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/glance/glance-registry-paste.ini' + end + + # added for backwards compatibility with older versions of inifile + def file_path + self.class.file_path + end + +end diff --git a/glance/lib/puppet/type/glance_api_config.rb b/glance/lib/puppet/type/glance_api_config.rb new file mode 100644 index 000000000..80c7f6cc8 --- /dev/null +++ b/glance/lib/puppet/type/glance_api_config.rb @@ -0,0 +1,43 @@ +Puppet::Type.newtype(:glance_api_config) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from glance-api.conf' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end + +end diff --git a/glance/lib/puppet/type/glance_api_paste_ini.rb b/glance/lib/puppet/type/glance_api_paste_ini.rb new file mode 100644 index 000000000..daf4cc498 --- /dev/null +++ b/glance/lib/puppet/type/glance_api_paste_ini.rb @@ -0,0 +1,43 @@ +Puppet::Type.newtype(:glance_api_paste_ini) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from glance-api-paste.ini' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end + +end diff --git a/glance/lib/puppet/type/glance_cache_config.rb b/glance/lib/puppet/type/glance_cache_config.rb new file mode 100644 index 000000000..5f801fd7a --- /dev/null +++ b/glance/lib/puppet/type/glance_cache_config.rb @@ -0,0 +1,43 @@ +Puppet::Type.newtype(:glance_cache_config) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from glance-cache.conf' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end + +end diff --git a/glance/lib/puppet/type/glance_image.rb b/glance/lib/puppet/type/glance_image.rb new file mode 100644 index 000000000..d63590d9c --- /dev/null +++ b/glance/lib/puppet/type/glance_image.rb @@ -0,0 +1,75 @@ +Puppet::Type.newtype(:glance_image) do + desc <<-EOT + This allows manifests to declare an image to be + stored in glance. + + glance_image { "Ubuntu 12.04 cloudimg amd64": + ensure => present, + name => "Ubuntu 12.04 cloudimg amd64" + is_public => yes, + container_format => ovf, + disk_format => 'qcow2', + source => 'http://uec-images.ubuntu.com/releases/precise/release/ubuntu-12.04-server-cloudimg-amd64-disk1.img' + } + + Known problems / limitations: + * All images are managed by the glance service. + This means that since users are unable to manage their own images via this type, + is_public is really of no use. You can probably hide images this way but that's all. + * As glance image names do not have to be unique, you must ensure that your glance + repository does not have any duplicate names prior to using this. + * Ensure this is run on the same server as the glance-api service. + + EOT + + ensurable + + newparam(:name, :namevar => true) do + desc 'The image name' + newvalues(/.*/) + end + + newproperty(:id) do + desc 'The unique id of the image' + validate do |v| + raise(Puppet::Error, 'This is a read only property') + end + end + + newproperty(:location) do + desc "The permanent location of the image. Optional" + newvalues(/\S+/) + end + + newproperty(:is_public) do + desc "Whether the image is public or not. Default true" + newvalues(/(y|Y)es/, /(n|N)o/) + defaultto('Yes') + munge do |v| + v.to_s.capitalize + end + end + + newproperty(:container_format) do + desc "The format of the container" + newvalues(:ami, :ari, :aki, :bare, :ovf) + end + + newproperty(:disk_format) do + desc "The format of the disk" + newvalues(:ami, :ari, :aki, :vhd, :vmd, :raw, :qcow2, :vdi, :iso) + end + + newparam(:source) do + desc "The source of the image to import from" + newvalues(/\S+/) + end + + # Require the Glance service to be running + autorequire(:service) do + ['glance'] + end + +end + + diff --git a/glance/lib/puppet/type/glance_registry_config.rb b/glance/lib/puppet/type/glance_registry_config.rb new file mode 100644 index 000000000..3291be391 --- /dev/null +++ b/glance/lib/puppet/type/glance_registry_config.rb @@ -0,0 +1,43 @@ +Puppet::Type.newtype(:glance_registry_config) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from glance-registry.conf' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end + +end diff --git a/glance/lib/puppet/type/glance_registry_paste_ini.rb b/glance/lib/puppet/type/glance_registry_paste_ini.rb new file mode 100644 index 000000000..1afeafd75 --- /dev/null +++ b/glance/lib/puppet/type/glance_registry_paste_ini.rb @@ -0,0 +1,43 @@ +Puppet::Type.newtype(:glance_registry_paste_ini) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from glance-registry-paste.ini' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end + +end diff --git a/glance/manifests/api.pp b/glance/manifests/api.pp new file mode 100644 index 000000000..a5745ad6e --- /dev/null +++ b/glance/manifests/api.pp @@ -0,0 +1,446 @@ +# == Class glance::api +# +# Configure API service in glance +# +# == Parameters +# +# [*keystone_password*] +# (required) Password used to authentication. +# +# [*verbose*] +# (optional) Rather to log the glance api service at verbose level. +# Default: false +# +# [*debug*] +# (optional) Rather to log the glance api service at debug level. +# Default: false +# +# [*bind_host*] +# (optional) The address of the host to bind to. +# Default: 0.0.0.0 +# +# [*bind_port*] +# (optional) The port the server should bind to. +# Default: 9292 +# +# [*backlog*] +# (optional) Backlog requests when creating socket +# Default: 4096 +# +# [*workers*] +# (optional) Number of Glance API worker processes to start +# Default: $::processorcount +# +# [*log_file*] +# (optional) The path of file used for logging +# If set to boolean false, it will not log to any file. +# Default: /var/log/glance/api.log +# +# [*log_dir*] +# (optional) directory to which glance logs are sent. +# If set to boolean false, it will not log to any directory. +# Defaults to '/var/log/glance' +# +# [*registry_host*] +# (optional) The address used to connect to the registry service. +# Default: 0.0.0.0 +# +# [*registry_port*] +# (optional) The port of the Glance registry service. +# Default: 9191 +# +# [*registry_client_protocol*] +# (optional) The protocol of the Glance registry service. +# Default: http +# +# [*auth_type*] +# (optional) Type is authorization being used. +# Defaults to 'keystone' +# +# [* auth_host*] +# (optional) Host running auth service. +# Defaults to '127.0.0.1'. +# +# [*auth_url*] +# (optional) Authentication URL. +# Defaults to 'http://localhost:5000/v2.0'. +# +# [* auth_port*] +# (optional) Port to use for auth service on auth_host. +# Defaults to '35357'. +# +# [* auth_uri*] +# (optional) Complete public Identity API endpoint. +# Defaults to false. +# +# [*auth_admin_prefix*] +# (optional) Path part of the auth url. +# This allow admin auth URIs like http://auth_host:35357/keystone/admin. +# (where '/keystone/admin' is auth_admin_prefix) +# Defaults to false for empty. If defined, should be a string with a leading '/' and no trailing '/'. +# +# [* auth_protocol*] +# (optional) Protocol to use for auth. +# Defaults to 'http'. +# +# [*pipeline*] +# (optional) Partial name of a pipeline in your paste configuration file with the +# service name removed. +# Defaults to 'keystone+cachemanagement'. +# +# [*keystone_tenant*] +# (optional) Tenant to authenticate to. +# Defaults to services. +# +# [*keystone_user*] +# (optional) User to authenticate as with keystone. +# Defaults to 'glance'. +# +# [*manage_service*] +# (optional) If Puppet should manage service startup / shutdown. +# Defaults to true. +# +# [*enabled*] +# (optional) Whether to enable services. +# Defaults to true. +# +# [*sql_idle_timeout*] +# (optional) Deprecated. Use database_idle_timeout instead +# Defaults to false +# +# [*sql_connection*] +# (optional) Deprecated. Use database_connection instead. +# Defaults to false +# +# [*database_connection*] +# (optional) Connection url to connect to nova database. +# Defaults to 'sqlite:///var/lib/glance/glance.sqlite' +# +# [*database_idle_timeout*] +# (optional) Timeout before idle db connections are reaped. +# Defaults to 3600 +# +# [*use_syslog*] +# (optional) Use syslog for logging. +# Defaults to false. +# +# [*log_facility*] +# (optional) Syslog facility to receive log lines. +# Defaults to 'LOG_USER'. +# +# [*show_image_direct_url*] +# (optional) Expose image location to trusted clients. +# Defaults to false. +# +# [*purge_config*] +# (optional) Whether to set only the specified config options +# in the api config. +# Defaults to false. +# +# [*cert_file*] +# (optinal) Certificate file to use when starting API server securely +# Defaults to false, not set +# +# [*key_file*] +# (optional) Private key file to use when starting API server securely +# Defaults to false, not set +# +# [*ca_file*] +# (optional) CA certificate file to use to verify connecting clients +# Defaults to false, not set +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +# [*known_stores*] +# (optional)List of which store classes and store class locations are +# currently known to glance at startup. +# Defaults to false. +# Example: ['glance.store.filesystem.Store','glance.store.http.Store'] +# +# [*image_cache_dir*] +# (optional) Base directory that the Image Cache uses. +# Defaults to '/var/lib/glance/image-cache'. +# +class glance::api( + $keystone_password, + $verbose = false, + $debug = false, + $bind_host = '0.0.0.0', + $bind_port = '9292', + $backlog = '4096', + $workers = $::processorcount, + $log_file = '/var/log/glance/api.log', + $log_dir = '/var/log/glance', + $registry_host = '0.0.0.0', + $registry_port = '9191', + $registry_client_protocol = 'http', + $auth_type = 'keystone', + $auth_host = '127.0.0.1', + $auth_url = 'http://localhost:5000/v2.0', + $auth_port = '35357', + $auth_uri = false, + $auth_admin_prefix = false, + $auth_protocol = 'http', + $pipeline = 'keystone+cachemanagement', + $keystone_tenant = 'services', + $keystone_user = 'glance', + $manage_service = true, + $enabled = true, + $use_syslog = false, + $log_facility = 'LOG_USER', + $show_image_direct_url = false, + $purge_config = false, + $cert_file = false, + $key_file = false, + $ca_file = false, + $known_stores = false, + $database_connection = 'sqlite:///var/lib/glance/glance.sqlite', + $database_idle_timeout = 3600, + $image_cache_dir = '/var/lib/glance/image-cache', + # DEPRECATED PARAMETERS + $mysql_module = undef, + $sql_idle_timeout = false, + $sql_connection = false, +) inherits glance { + + require keystone::python + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + if ( $glance::params::api_package_name != $glance::params::registry_package_name ) { + ensure_packages([$glance::params::api_package_name]) + } + + Package[$glance::params::api_package_name] -> File['/etc/glance/'] + Package[$glance::params::api_package_name] -> Glance_api_config<||> + Package[$glance::params::api_package_name] -> Glance_cache_config<||> + + # adding all of this stuff b/c it devstack says glance-api uses the + # db now + Glance_api_config<||> ~> Exec<| title == 'glance-manage db_sync' |> + Glance_cache_config<||> ~> Exec<| title == 'glance-manage db_sync' |> + Exec<| title == 'glance-manage db_sync' |> ~> Service['glance-api'] + Glance_api_config<||> ~> Service['glance-api'] + Glance_cache_config<||> ~> Service['glance-api'] + + File { + ensure => present, + owner => 'glance', + group => 'glance', + mode => '0640', + notify => Service['glance-api'], + require => Class['glance'] + } + + if $sql_connection { + warning('The sql_connection parameter is deprecated, use database_connection instead.') + $database_connection_real = $sql_connection + } else { + $database_connection_real = $database_connection + } + + if $sql_idle_timeout { + warning('The sql_idle_timeout parameter is deprecated, use database_idle_timeout instead.') + $database_idle_timeout_real = $sql_idle_timeout + } else { + $database_idle_timeout_real = $database_idle_timeout + } + + if $database_connection_real { + if($database_connection_real =~ /mysql:\/\/\S+:\S+@\S+\/\S+/) { + require 'mysql::bindings' + require 'mysql::bindings::python' + } elsif($database_connection_real =~ /postgresql:\/\/\S+:\S+@\S+\/\S+/) { + + } elsif($database_connection_real =~ /sqlite:\/\//) { + + } else { + fail("Invalid db connection ${database_connection_real}") + } + glance_api_config { + 'database/connection': value => $database_connection_real, secret => true; + 'database/idle_timeout': value => $database_idle_timeout_real; + } + } + + # basic service config + glance_api_config { + 'DEFAULT/verbose': value => $verbose; + 'DEFAULT/debug': value => $debug; + 'DEFAULT/bind_host': value => $bind_host; + 'DEFAULT/bind_port': value => $bind_port; + 'DEFAULT/backlog': value => $backlog; + 'DEFAULT/workers': value => $workers; + 'DEFAULT/show_image_direct_url': value => $show_image_direct_url; + 'DEFAULT/image_cache_dir': value => $image_cache_dir; + } + + # known_stores config + if $known_stores { + glance_api_config { + 'DEFAULT/known_stores': value => join($known_stores, ','); + } + } else { + glance_api_config { + 'DEFAULT/known_stores': ensure => absent; + } + } + + glance_cache_config { + 'DEFAULT/verbose': value => $verbose; + 'DEFAULT/debug': value => $debug; + } + + # configure api service to connect registry service + glance_api_config { + 'DEFAULT/registry_host': value => $registry_host; + 'DEFAULT/registry_port': value => $registry_port; + 'DEFAULT/registry_client_protocol': value => $registry_client_protocol; + } + + glance_cache_config { + 'DEFAULT/registry_host': value => $registry_host; + 'DEFAULT/registry_port': value => $registry_port; + } + + if $auth_uri { + glance_api_config { 'keystone_authtoken/auth_uri': value => $auth_uri; } + } else { + glance_api_config { 'keystone_authtoken/auth_uri': value => "${auth_protocol}://${auth_host}:5000/"; } + } + + # auth config + glance_api_config { + 'keystone_authtoken/auth_host': value => $auth_host; + 'keystone_authtoken/auth_port': value => $auth_port; + 'keystone_authtoken/auth_protocol': value => $auth_protocol; + } + + if $auth_admin_prefix { + validate_re($auth_admin_prefix, '^(/.+[^/])?$') + glance_api_config { + 'keystone_authtoken/auth_admin_prefix': value => $auth_admin_prefix; + } + } else { + glance_api_config { + 'keystone_authtoken/auth_admin_prefix': ensure => absent; + } + } + + # Set the pipeline, it is allowed to be blank + if $pipeline != '' { + validate_re($pipeline, '^(\w+([+]\w+)*)*$') + glance_api_config { + 'paste_deploy/flavor': + ensure => present, + value => $pipeline, + } + } else { + glance_api_config { 'paste_deploy/flavor': ensure => absent } + } + + # keystone config + if $auth_type == 'keystone' { + glance_api_config { + 'keystone_authtoken/admin_tenant_name': value => $keystone_tenant; + 'keystone_authtoken/admin_user' : value => $keystone_user; + 'keystone_authtoken/admin_password' : value => $keystone_password, secret => true; + } + glance_cache_config { + 'DEFAULT/auth_url' : value => $auth_url; + 'DEFAULT/admin_tenant_name': value => $keystone_tenant; + 'DEFAULT/admin_user' : value => $keystone_user; + 'DEFAULT/admin_password' : value => $keystone_password, secret => true; + } + } + + # SSL Options + if $cert_file { + glance_api_config { + 'DEFAULT/cert_file' : value => $cert_file; + } + } else { + glance_api_config { + 'DEFAULT/cert_file': ensure => absent; + } + } + if $key_file { + glance_api_config { + 'DEFAULT/key_file' : value => $key_file; + } + } else { + glance_api_config { + 'DEFAULT/key_file': ensure => absent; + } + } + if $ca_file { + glance_api_config { + 'DEFAULT/ca_file' : value => $ca_file; + } + } else { + glance_api_config { + 'DEFAULT/ca_file': ensure => absent; + } + } + + # Logging + if $log_file { + glance_api_config { + 'DEFAULT/log_file': value => $log_file; + } + } else { + glance_api_config { + 'DEFAULT/log_file': ensure => absent; + } + } + + if $log_dir { + glance_api_config { + 'DEFAULT/log_dir': value => $log_dir; + } + } else { + glance_api_config { + 'DEFAULT/log_dir': ensure => absent; + } + } + + # Syslog + if $use_syslog { + glance_api_config { + 'DEFAULT/use_syslog' : value => true; + 'DEFAULT/syslog_log_facility' : value => $log_facility; + } + } else { + glance_api_config { + 'DEFAULT/use_syslog': value => false; + } + } + + resources { 'glance_api_config': + purge => $purge_config, + } + + file { ['/etc/glance/glance-api.conf', + '/etc/glance/glance-api-paste.ini', + '/etc/glance/glance-cache.conf']: + } + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + service { 'glance-api': + ensure => $service_ensure, + name => $::glance::params::api_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + } +} diff --git a/glance/manifests/backend/cinder.pp b/glance/manifests/backend/cinder.pp new file mode 100644 index 000000000..db9593338 --- /dev/null +++ b/glance/manifests/backend/cinder.pp @@ -0,0 +1,97 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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: glance::backend::cinder +# +# Setup Glance to backend images into Cinder +# +# === Parameters +# +# [*cinder_catalog_info*] +# (optional) Info to match when looking for cinder in the service catalog. +# Format is : separated values of the form: +# :: (string value) +# Defaults to 'volume:cinder:publicURL' +# +# [*cinder_endpoint_template*] +# (optional) Override service catalog lookup with template for cinder endpoint. +# Should be a valid URL. Example: 'http://localhost:8776/v1/%(project_id)s' +# Defaults to 'undef' +# +# [*os_region_name*] +# (optional) Region name of this node. +# Should be a valid region name +# Defaults to 'RegionOne' +# +# [*cinder_ca_certificates_file*] +# (optional) Location of ca certicate file to use for cinder client requests. +# Should be a valid ca certicate file +# Defaults to undef +# +# [*cinder_http_retries*] +# (optional) Number of cinderclient retries on failed http calls. +# Should be a valid integer +# Defaults to '3' +# +# [*cinder_api_insecure*] +# (optional) Allow to perform insecure SSL requests to cinder. +# Should be a valid boolean value +# Defaults to false +# + +class glance::backend::cinder( + $os_region_name = 'RegionOne', + $cinder_ca_certificates_file = undef, + $cinder_api_insecure = false, + $cinder_catalog_info = 'volume:cinder:publicURL', + $cinder_endpoint_template = undef, + $cinder_http_retries = '3' + +) { + + glance_api_config { + 'DEFAULT/cinder_api_insecure': value => $cinder_api_insecure; + 'DEFAULT/cinder_catalog_info': value => $cinder_catalog_info; + 'DEFAULT/cinder_http_retries': value => $cinder_http_retries; + 'DEFAULT/default_store': value => 'cinder'; + 'DEFAULT/os_region_name': value => $os_region_name; + } + + glance_cache_config { + 'DEFAULT/cinder_api_insecure': value => $cinder_api_insecure; + 'DEFAULT/cinder_catalog_info': value => $cinder_catalog_info; + 'DEFAULT/cinder_http_retries': value => $cinder_http_retries; + 'DEFAULT/os_region_name': value => $os_region_name; + } + + if $cinder_endpoint_template { + glance_api_config { 'DEFAULT/cinder_endpoint_template': value => $cinder_endpoint_template; } + glance_cache_config { 'DEFAULT/cinder_endpoint_template': value => $cinder_endpoint_template; } + } else { + glance_api_config { 'DEFAULT/cinder_endpoint_template': ensure => absent; } + glance_cache_config { 'DEFAULT/cinder_endpoint_template': ensure => absent; } + } + + if $cinder_ca_certificates_file { + glance_api_config { 'DEFAULT/cinder_ca_certificates_file': value => $cinder_ca_certificates_file; } + glance_cache_config { 'DEFAULT/cinder_ca_certificates_file': value => $cinder_ca_certificates_file; } + } else { + glance_api_config { 'DEFAULT/cinder_ca_certificates_file': ensure => absent; } + glance_cache_config { 'DEFAULT/cinder_ca_certificates_file': ensure => absent; } + } + +} diff --git a/glance/manifests/backend/file.pp b/glance/manifests/backend/file.pp new file mode 100644 index 000000000..9c27719ba --- /dev/null +++ b/glance/manifests/backend/file.pp @@ -0,0 +1,19 @@ +# +# used to configure file backends for glance +# +# $filesystem_store_datadir - Location where dist images are stored when +# default_store == file. +# Optional. Default: /var/lib/glance/images/ +class glance::backend::file( + $filesystem_store_datadir = '/var/lib/glance/images/' +) inherits glance::api { + + glance_api_config { + 'DEFAULT/default_store': value => 'file'; + 'DEFAULT/filesystem_store_datadir': value => $filesystem_store_datadir; + } + + glance_cache_config { + 'DEFAULT/filesystem_store_datadir': value => $filesystem_store_datadir; + } +} diff --git a/glance/manifests/backend/rbd.pp b/glance/manifests/backend/rbd.pp new file mode 100644 index 000000000..9ff56de7c --- /dev/null +++ b/glance/manifests/backend/rbd.pp @@ -0,0 +1,49 @@ +# +# configures the storage backend for glance +# as a rbd instance +# +# $rbd_store_user - Optional. +# +# $rbd_store_pool - Optional. Default:'images' +# +# $rbd_store_ceph_conf - Optional. Default:'/etc/ceph/ceph.conf' +# +# $rbd_store_chunk_size - Optional. Default:'8' +# +# $show_image_direct_url - Optional. Enables direct COW from glance to rbd +# DEPRECATED, use show_image_direct_url in glance::api +# +# [*package_ensure*] +# (optional) Desired ensure state of packages. +# accepts latest or specific versions. +# Defaults to present. +# + +class glance::backend::rbd( + $rbd_store_user = undef, + $rbd_store_ceph_conf = '/etc/ceph/ceph.conf', + $rbd_store_pool = 'images', + $rbd_store_chunk_size = '8', + $show_image_direct_url = undef, + $package_ensure = 'present', +) { + include glance::params + + if $show_image_direct_url { + notice('parameter show_image_direct_url is deprecated, use parameter in glance::api') + } + + glance_api_config { + 'DEFAULT/default_store': value => 'rbd'; + 'DEFAULT/rbd_store_ceph_conf': value => $rbd_store_ceph_conf; + 'DEFAULT/rbd_store_user': value => $rbd_store_user; + 'DEFAULT/rbd_store_pool': value => $rbd_store_pool; + 'DEFAULT/rbd_store_chunk_size': value => $rbd_store_chunk_size; + } + + package { 'python-ceph': + ensure => $package_ensure, + name => $::glance::params::pyceph_package_name, + } + +} diff --git a/glance/manifests/backend/swift.pp b/glance/manifests/backend/swift.pp new file mode 100644 index 000000000..883125785 --- /dev/null +++ b/glance/manifests/backend/swift.pp @@ -0,0 +1,53 @@ +# +# configures the storage backend for glance +# as a swift instance +# +# $swift_store_user - Required. +# +# $swift_store_key - Required. +# +# $swift_store_auth_address - Optional. Default: '127.0.0.1:5000/v2.0/' +# +# $swift_store_container - Optional. Default: 'glance' +# +# $swift_store_auth_version - Optional. Default: '2' +# +# $swift_store_create_container_on_put - Optional. Default: 'False' +# +# $swift_store_large_object_size - Optional. Default: '5120' +class glance::backend::swift( + $swift_store_user, + $swift_store_key, + $swift_store_auth_address = '127.0.0.1:5000/v2.0/', + $swift_store_container = 'glance', + $swift_store_auth_version = '2', + $swift_store_large_object_size = '5120', + $swift_store_create_container_on_put = false +) { + + glance_api_config { + 'DEFAULT/default_store': value => 'swift'; + 'DEFAULT/swift_store_user': value => $swift_store_user; + 'DEFAULT/swift_store_key': value => $swift_store_key; + 'DEFAULT/swift_store_auth_address': value => $swift_store_auth_address; + 'DEFAULT/swift_store_container': value => $swift_store_container; + 'DEFAULT/swift_store_auth_version': value => $swift_store_auth_version; + 'DEFAULT/swift_store_create_container_on_put': + value => $swift_store_create_container_on_put; + 'DEFAULT/swift_store_large_object_size': + value => $swift_store_large_object_size; + } + + glance_cache_config { + 'DEFAULT/swift_store_user': value => $swift_store_user; + 'DEFAULT/swift_store_key': value => $swift_store_key; + 'DEFAULT/swift_store_auth_address': value => $swift_store_auth_address; + 'DEFAULT/swift_store_container': value => $swift_store_container; + 'DEFAULT/swift_store_auth_version': value => $swift_store_auth_version; + 'DEFAULT/swift_store_create_container_on_put': + value => $swift_store_create_container_on_put; + 'DEFAULT/swift_store_large_object_size': + value => $swift_store_large_object_size; + } + +} diff --git a/glance/manifests/cache/cleaner.pp b/glance/manifests/cache/cleaner.pp new file mode 100644 index 000000000..f05258db2 --- /dev/null +++ b/glance/manifests/cache/cleaner.pp @@ -0,0 +1,49 @@ +# == Class: glance::cache::cleaner +# +# Installs a cron job to run glance-cache-cleaner. +# +# === Parameters +# +# [*minute*] +# (optional) Defaults to '1'. +# +# [*hour*] +# (optional) Defaults to '0'. +# +# [*monthday*] +# (optional) Defaults to '*'. +# +# [*month*] +# (optional) Defaults to '*'. +# +# [*weekday*] +# (optional) Defaults to '*'. +# +# [*command_options*] +# command options to add to the cronjob +# (eg. point to config file, or redirect output) +# (optional) Defaults to ''. +# +class glance::cache::cleaner ( + $minute = 1, + $hour = 0, + $monthday = '*', + $month = '*', + $weekday = '*', + $command_options = '', +) { + + include glance::params + + cron { 'glance-cache-cleaner': + command => "${glance::params::cache_cleaner_command} ${command_options}", + environment => 'PATH=/bin:/usr/bin:/usr/sbin', + user => 'glance', + minute => $minute, + hour => $hour, + monthday => $monthday, + month => $month, + weekday => $weekday, + require => Package[$::glance::params::api_package_name], + } +} diff --git a/glance/manifests/cache/pruner.pp b/glance/manifests/cache/pruner.pp new file mode 100644 index 000000000..96e135fb2 --- /dev/null +++ b/glance/manifests/cache/pruner.pp @@ -0,0 +1,50 @@ +# == Class: glance::cache::pruner +# +# Installs a cron job to run glance-cache-pruner. +# +# === Parameters +# +# [*minute*] +# (optional) Defaults to '*/30'. +# +# [*hour*] +# (optional) Defaults to '*'. +# +# [*monthday*] +# (optional) Defaults to '*'. +# +# [*month*] +# (optional) Defaults to '*'. +# +# [*weekday*] +# (optional) Defaults to '*'. +# +# [*command_options*] +# command options to add to the cronjob +# (eg. point to config file, or redirect output) +# (optional) Defaults to ''. +# +class glance::cache::pruner ( + $minute = '*/30', + $hour = '*', + $monthday = '*', + $month = '*', + $weekday = '*', + $command_options = '', +) { + + include glance::params + + cron { 'glance-cache-pruner': + command => "${glance::params::cache_pruner_command} ${command_options}", + environment => 'PATH=/bin:/usr/bin:/usr/sbin', + user => 'glance', + minute => $minute, + hour => $hour, + monthday => $monthday, + month => $month, + weekday => $weekday, + require => Package[$::glance::params::api_package_name], + + } +} diff --git a/glance/manifests/client.pp b/glance/manifests/client.pp new file mode 100644 index 000000000..cbb0f1c0c --- /dev/null +++ b/glance/manifests/client.pp @@ -0,0 +1,18 @@ +# +# Installs the glance python library. +# +# == parameters +# * ensure - ensure state for pachage. +# +class glance::client ( + $ensure = 'present' +) { + + include glance::params + + package { 'python-glanceclient': + ensure => $ensure, + name => $::glance::params::client_package_name, + } + +} diff --git a/glance/manifests/config.pp b/glance/manifests/config.pp new file mode 100644 index 000000000..d9c6a4371 --- /dev/null +++ b/glance/manifests/config.pp @@ -0,0 +1,56 @@ +# == Class: glance::config +# +# This class is used to manage arbitrary glance configurations. +# +# === Parameters +# +# [*xxx_config*] +# (optional) Allow configuration of arbitrary glance configurations. +# The value is an hash of glance_config resources. Example: +# { 'DEFAULT/foo' => { value => 'fooValue'}, +# 'DEFAULT/bar' => { value => 'barValue'} +# } +# In yaml format, Example: +# glance_config: +# DEFAULT/foo: +# value: fooValue +# DEFAULT/bar: +# value: barValue +# +# [**api_config**] +# (optional) Allow configuration of glance-api.conf configurations. +# +# [**api_paste_ini_config**] +# (optional) Allow configuration of glance-api-paste.ini configurations. +# +# [**registry_config**] +# (optional) Allow configuration of glance-registry.conf configurations. +# +# [**registry_paste_ini_config**] +# (optional) Allow configuration of glance-registry-paste.ini configurations. +# +# [**cache_config**] +# (optional) Allow configuration of glance-cache.conf configurations. +# +# NOTE: The configuration MUST NOT be already handled by this module +# or Puppet catalog compilation will fail with duplicate resources. +# +class glance::config ( + $api_config = {}, + $api_paste_ini_config = {}, + $registry_config = {}, + $registry_paste_ini_config = {}, + $cache_config = {}, +) { + validate_hash($api_config) + validate_hash($api_paste_ini_config) + validate_hash($registry_config) + validate_hash($registry_paste_ini_config) + validate_hash($cache_config) + + create_resources('glance_api_config', $api_config) + create_resources('glance_api_paste_ini', $api_paste_ini_config) + create_resources('glance_registry_config', $registry_config) + create_resources('glance_registry_paste_ini', $registry_paste_ini_config) + create_resources('glance_cache_config', $cache_config) +} diff --git a/glance/manifests/db/mysql.pp b/glance/manifests/db/mysql.pp new file mode 100644 index 000000000..e4df4c894 --- /dev/null +++ b/glance/manifests/db/mysql.pp @@ -0,0 +1,62 @@ +# The glance::db::mysql class creates a MySQL database for glance. +# It must be used on the MySQL server +# +# == Parameters +# +# [*password*] +# password to connect to the database. Mandatory. +# +# [*dbname*] +# name of the database. Optional. Defaults to glance. +# +# [*user*] +# user to connect to the database. Optional. Defaults to glance. +# +# [*host*] +# the default source host user is allowed to connect from. +# Optional. Defaults to 'localhost' +# +# [*allowed_hosts*] +# other hosts the user is allowd to connect from. +# Optional. Defaults to undef. +# +# [*charset*] +# the database charset. Optional. Defaults to 'utf8' +# +# [*collate*] +# the database collation. Optional. Defaults to 'utf8_unicode_ci' +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +class glance::db::mysql( + $password, + $dbname = 'glance', + $user = 'glance', + $host = '127.0.0.1', + $allowed_hosts = undef, + $charset = 'utf8', + $collate = 'utf8_unicode_ci', + $cluster_id = 'localzone', + $mysql_module = undef, +) { + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + validate_string($password) + + ::openstacklib::db::mysql { 'glance': + user => $user, + password_hash => mysql_password($password), + dbname => $dbname, + host => $host, + charset => $charset, + collate => $collate, + allowed_hosts => $allowed_hosts, + } + + ::Openstacklib::Db::Mysql['glance'] ~> Exec<| title == 'glance-manage db_sync' |> + +} diff --git a/glance/manifests/db/postgresql.pp b/glance/manifests/db/postgresql.pp new file mode 100644 index 000000000..a1eb54c63 --- /dev/null +++ b/glance/manifests/db/postgresql.pp @@ -0,0 +1,21 @@ +# +# Class that configures postgresql for glance +# +# Requires the Puppetlabs postgresql module. +class glance::db::postgresql( + $password, + $dbname = 'glance', + $user = 'glance' +) { + + require postgresql::python + + Postgresql::Db[$dbname] ~> Exec<| title == 'glance-manage db_sync' |> + Package['python-psycopg2'] -> Exec<| title == 'glance-manage db_sync' |> + + postgresql::db { $dbname: + user => $user, + password => $password, + } + +} diff --git a/glance/manifests/init.pp b/glance/manifests/init.pp new file mode 100644 index 000000000..6414f771a --- /dev/null +++ b/glance/manifests/init.pp @@ -0,0 +1,26 @@ +# +# base glacne config. +# +# == parameters +# * package_ensure - ensure state for package. +# +class glance( + $package_ensure = 'present' +) { + + include glance::params + + file { '/etc/glance/': + ensure => directory, + owner => 'glance', + group => 'root', + mode => '0770', + } + + if ( $glance::params::api_package_name == $glance::params::registry_package_name ) { + package { $glance::params::api_package_name : + ensure => $package_ensure, + name => $::glance::params::package_name, + } + } +} diff --git a/glance/manifests/keystone/auth.pp b/glance/manifests/keystone/auth.pp new file mode 100644 index 000000000..d325acef1 --- /dev/null +++ b/glance/manifests/keystone/auth.pp @@ -0,0 +1,85 @@ +# +# Sets up glance users, service and endpoint +# +# == Parameters: +# +# $auth_name :: identifier used for all keystone objects related to glance. +# Optional. Defaults to glance. +# $password :: password for glance user. Optional. Defaults to glance_password. +# $configure_user :: Whether to configure a service user. Optional. Defaults to true. +# $configure_user_role :: Whether to configure the admin role for the service user. +# Optional. Defaults to true. +# $service_name :: name of the service. Optional. Defaults to value of auth_name. +# $service_type :: type of service to create. Optional. Defaults to image. +# $public_address :: Public address for endpoint. Optional. Defaults to 127.0.0.1. +# $admin_address :: Admin address for endpoint. Optional. Defaults to 127.0.0.1. +# $inernal_address :: Internal address for endpoint. Optional. Defaults to 127.0.0.1. +# $port :: Port for endpoint. Needs to match glance api service port. Optional. +# Defaults to 9292. +# $region :: Region where endpoint is set. +# $public_protocol :: Protocol for public endpoint. Optional. Defaults to http. +# $admin_protocol :: Protocol for admin endpoint. Optional. Defaults to http. +# $internal_protocol :: Protocol for internal endpoint. Optional. Defaults to http. +# +class glance::keystone::auth( + $password, + $email = 'glance@localhost', + $auth_name = 'glance', + $configure_endpoint = true, + $configure_user = true, + $configure_user_role = true, + $service_name = undef, + $service_type = 'image', + $public_address = '127.0.0.1', + $admin_address = '127.0.0.1', + $internal_address = '127.0.0.1', + $port = '9292', + $region = 'RegionOne', + $tenant = 'services', + $public_protocol = 'http', + $admin_protocol = 'http', + $internal_protocol = 'http' +) { + + if $service_name == undef { + $real_service_name = $auth_name + } else { + $real_service_name = $service_name + } + + Keystone_endpoint["${region}/${real_service_name}"] ~> Service <| name == 'glance-api' |> + + if $configure_user { + keystone_user { $auth_name: + ensure => present, + password => $password, + email => $email, + tenant => $tenant, + } + } + + if $configure_user_role { + Keystone_user_role["${auth_name}@${tenant}"] ~> Service <| name == 'glance-registry' |> + Keystone_user_role["${auth_name}@${tenant}"] ~> Service <| name == 'glance-api' |> + + keystone_user_role { "${auth_name}@${tenant}": + ensure => present, + roles => 'admin', + } + } + + keystone_service { $real_service_name: + ensure => present, + type => $service_type, + description => 'Openstack Image Service', + } + + if $configure_endpoint { + keystone_endpoint { "${region}/${real_service_name}": + ensure => present, + public_url => "${public_protocol}://${public_address}:${port}", + admin_url => "${admin_protocol}://${admin_address}:${port}", + internal_url => "${internal_protocol}://${internal_address}:${port}", + } + } +} diff --git a/glance/manifests/notify/qpid.pp b/glance/manifests/notify/qpid.pp new file mode 100644 index 000000000..af1ab7814 --- /dev/null +++ b/glance/manifests/notify/qpid.pp @@ -0,0 +1,21 @@ +# +# used to configure qpid notifications for glance +# +class glance::notify::qpid( + $qpid_password, + $qpid_username = 'guest', + $qpid_hostname = 'localhost', + $qpid_port = '5672', + $qpid_protocol = 'tcp' +) inherits glance::api { + + glance_api_config { + 'DEFAULT/notifier_driver': value => 'qpid'; + 'DEFAULT/qpid_hostname': value => $qpid_hostname; + 'DEFAULT/qpid_port': value => $qpid_port; + 'DEFAULT/qpid_protocol': value => $qpid_protocol; + 'DEFAULT/qpid_username': value => $qpid_username; + 'DEFAULT/qpid_password': value => $qpid_password, secret => true; + } + +} diff --git a/glance/manifests/notify/rabbitmq.pp b/glance/manifests/notify/rabbitmq.pp new file mode 100644 index 000000000..7f6fcf9ff --- /dev/null +++ b/glance/manifests/notify/rabbitmq.pp @@ -0,0 +1,116 @@ +# +# used to configure rabbitmq notifications for glance +# +# [*rabbit_password*] +# password to connect to the rabbit_server. +# [*rabbit_userid*] +# user to connect to the rabbit server. Optional. Defaults to 'guest' +# [*rabbit_host*] +# ip or hostname of the rabbit server. Optional. Defaults to 'localhost' +# [*rabbit_port*] +# port of the rabbit server. Optional. Defaults to 5672. +# [*rabbit_virtual_host*] +# virtual_host to use. Optional. Defaults to '/' +# [*rabbit_use_ssl*] +# (optional) Connect over SSL for RabbitMQ +# Defaults to false +# [*kombu_ssl_ca_certs*] +# (optional) SSL certification authority file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_certfile*] +# (optional) SSL cert file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_keyfile*] +# (optional) SSL key file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_version*] +# (optional) SSL version to use (valid only if SSL enabled). +# Valid values are TLSv1, SSLv23 and SSLv3. SSLv2 may be +# available on some distributions. +# Defaults to 'SSLv3' +# [*rabbit_notification_exchange*] +# Defaults to 'glance' +# [*rabbit_notification_topic*] +# Defaults to 'notifications' +# [*rabbit_durable_queues*] +# Defaults to false +# +class glance::notify::rabbitmq( + $rabbit_password, + $rabbit_userid = 'guest', + $rabbit_host = 'localhost', + $rabbit_port = '5672', + $rabbit_hosts = false, + $rabbit_virtual_host = '/', + $rabbit_use_ssl = false, + $kombu_ssl_ca_certs = undef, + $kombu_ssl_certfile = undef, + $kombu_ssl_keyfile = undef, + $kombu_ssl_version = 'SSLv3', + $rabbit_notification_exchange = 'glance', + $rabbit_notification_topic = 'notifications', + $rabbit_durable_queues = false, + $amqp_durable_queues = false, +) { + + if $rabbit_durable_queues { + warning('The rabbit_durable_queues parameter is deprecated, use amqp_durable_queues.') + $amqp_durable_queues_real = $rabbit_durable_queues + } else { + $amqp_durable_queues_real = $amqp_durable_queues + } + + if $rabbit_use_ssl { + if !$kombu_ssl_ca_certs { + fail('The kombu_ssl_ca_certs parameter is required when rabbit_use_ssl is set to true') + } + if !$kombu_ssl_certfile { + fail('The kombu_ssl_certfile parameter is required when rabbit_use_ssl is set to true') + } + if !$kombu_ssl_keyfile { + fail('The kombu_ssl_keyfile parameter is required when rabbit_use_ssl is set to true') + } + } + + if $rabbit_hosts { + glance_api_config { + 'DEFAULT/rabbit_hosts': value => join($rabbit_hosts, ','); + 'DEFAULT/rabbit_ha_queues': value => true + } + } else { + glance_api_config { + 'DEFAULT/rabbit_host': value => $rabbit_host; + 'DEFAULT/rabbit_port': value => $rabbit_port; + 'DEFAULT/rabbit_hosts': value => "${rabbit_host}:${rabbit_port}"; + 'DEFAULT/rabbit_ha_queues': value => false + } + } + + glance_api_config { + 'DEFAULT/notification_driver': value => 'messaging'; + 'DEFAULT/rabbit_virtual_host': value => $rabbit_virtual_host; + 'DEFAULT/rabbit_password': value => $rabbit_password, secret => true; + 'DEFAULT/rabbit_userid': value => $rabbit_userid; + 'DEFAULT/rabbit_notification_exchange': value => $rabbit_notification_exchange; + 'DEFAULT/rabbit_notification_topic': value => $rabbit_notification_topic; + 'DEFAULT/rabbit_use_ssl': value => $rabbit_use_ssl; + 'DEFAULT/amqp_durable_queues': value => $amqp_durable_queues_real; + } + + if $rabbit_use_ssl { + glance_api_config { + 'DEFAULT/kombu_ssl_ca_certs': value => $kombu_ssl_ca_certs; + 'DEFAULT/kombu_ssl_certfile': value => $kombu_ssl_certfile; + 'DEFAULT/kombu_ssl_keyfile': value => $kombu_ssl_keyfile; + 'DEFAULT/kombu_ssl_version': value => $kombu_ssl_version; + } + } else { + glance_api_config { + 'DEFAULT/kombu_ssl_ca_certs': ensure => absent; + 'DEFAULT/kombu_ssl_certfile': ensure => absent; + 'DEFAULT/kombu_ssl_keyfile': ensure => absent; + 'DEFAULT/kombu_ssl_version': ensure => absent; + } + } + +} diff --git a/glance/manifests/params.pp b/glance/manifests/params.pp new file mode 100644 index 000000000..746cdbc41 --- /dev/null +++ b/glance/manifests/params.pp @@ -0,0 +1,31 @@ +# these parameters need to be accessed from several locations and +# should be considered to be constant +class glance::params { + + $client_package_name = 'python-glanceclient' + $pyceph_package_name = 'python-ceph' + + $cache_cleaner_command = 'glance-cache-cleaner' + $cache_pruner_command = 'glance-cache-pruner' + + case $::osfamily { + 'RedHat': { + $api_package_name = 'openstack-glance' + $registry_package_name = 'openstack-glance' + $api_service_name = 'openstack-glance-api' + $registry_service_name = 'openstack-glance-registry' + $db_sync_command = 'glance-manage db_sync' + } + 'Debian': { + $api_package_name = 'glance-api' + $registry_package_name = 'glance-registry' + $api_service_name = 'glance-api' + $registry_service_name = 'glance-registry' + $db_sync_command = 'glance-manage db_sync' + } + default: { + fail("Unsupported osfamily: ${::osfamily} operatingsystem: ${::operatingsystem}, module ${module_name} only support osfamily RedHat and Debian") + } + } + +} diff --git a/glance/manifests/registry.pp b/glance/manifests/registry.pp new file mode 100644 index 000000000..da242e37f --- /dev/null +++ b/glance/manifests/registry.pp @@ -0,0 +1,355 @@ +# == Class: glance::registry +# +# Installs and configures glance-registry +# +# === Parameters +# +# [*keystone_password*] +# (required) The keystone password for administrative user +# +# [*verbose*] +# (optional) Enable verbose logs (true|false). Defaults to false. +# +# [*debug*] +# (optional) Enable debug logs (true|false). Defaults to false. +# +# [*bind_host*] +# (optional) The address of the host to bind to. Defaults to '0.0.0.0'. +# +# [*bind_port*] +# (optional) The port the server should bind to. Defaults to '9191'. +# +# [*log_file*] +# (optional) Log file for glance-registry. +# If set to boolean false, it will not log to any file. +# Defaults to '/var/log/glance/registry.log'. +# +# [*log_dir*] +# (optional) directory to which glance logs are sent. +# If set to boolean false, it will not log to any directory. +# Defaults to '/var/log/glance' +# +# [*sql_idle_timeout*] +# (optional) Deprecated. Use database_idle_timeout instead +# Defaults to false +# +# [*sql_connection*] +# (optional) Deprecated. Use database_connection instead. +# Defaults to false +# +# [*database_connection*] +# (optional) Connection url to connect to nova database. +# Defaults to 'sqlite:///var/lib/glance/glance.sqlite' +# +# [*database_idle_timeout*] +# (optional) Timeout before idle db connections are reaped. +# Defaults to 3600 +# +# [*auth_type*] +# (optional) Authentication type. Defaults to 'keystone'. +# +# [*auth_host*] +# (optional) Address of the admin authentication endpoint. +# Defaults to '127.0.0.1'. +# +# [*auth_port*] +# (optional) Port of the admin authentication endpoint. Defaults to '35357'. +# +# [*auth_admin_prefix*] +# (optional) path part of the auth url. +# This allow admin auth URIs like http://auth_host:35357/keystone/admin. +# (where '/keystone/admin' is auth_admin_prefix) +# Defaults to false for empty. If defined, should be a string with a leading '/' and no trailing '/'. +# +# [*auth_protocol*] +# (optional) Protocol to communicate with the admin authentication endpoint. +# Defaults to 'http'. Should be 'http' or 'https'. +# +# [*auth_uri*] +# (optional) Complete public Identity API endpoint. +# +# [*keystone_tenant*] +# (optional) administrative tenant name to connect to keystone. +# Defaults to 'services'. +# +# [*keystone_user*] +# (optional) administrative user name to connect to keystone. +# Defaults to 'glance'. +# +# [*use_syslog*] +# (optional) Use syslog for logging. +# Defaults to false. +# +# [*log_facility*] +# (optional) Syslog facility to receive log lines. +# Defaults to LOG_USER. +# +# [*manage_service*] +# (optional) If Puppet should manage service startup / shutdown. +# Defaults to true. +# +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true. +# +# [*purge_config*] +# (optional) Whether to create only the specified config values in +# the glance registry config file. +# Defaults to false. +# +# [*cert_file*] +# (optinal) Certificate file to use when starting registry server securely +# Defaults to false, not set +# +# [*key_file*] +# (optional) Private key file to use when starting registry server securely +# Defaults to false, not set +# +# [*ca_file*] +# (optional) CA certificate file to use to verify connecting clients +# Defaults to false, not set +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +class glance::registry( + $keystone_password, + $verbose = false, + $debug = false, + $bind_host = '0.0.0.0', + $bind_port = '9191', + $log_file = '/var/log/glance/registry.log', + $log_dir = '/var/log/glance', + $database_connection = 'sqlite:///var/lib/glance/glance.sqlite', + $database_idle_timeout = 3600, + $auth_type = 'keystone', + $auth_host = '127.0.0.1', + $auth_port = '35357', + $auth_admin_prefix = false, + $auth_uri = false, + $auth_protocol = 'http', + $keystone_tenant = 'services', + $keystone_user = 'glance', + $pipeline = 'keystone', + $use_syslog = false, + $log_facility = 'LOG_USER', + $manage_service = true, + $enabled = true, + $purge_config = false, + $cert_file = false, + $key_file = false, + $ca_file = false, + # DEPRECATED PARAMETERS + $mysql_module = undef, + $sql_idle_timeout = false, + $sql_connection = false, +) inherits glance { + + require keystone::python + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + if ( $glance::params::api_package_name != $glance::params::registry_package_name ) { + ensure_packages([$glance::params::registry_package_name]) + } + + Package[$glance::params::registry_package_name] -> File['/etc/glance/'] + Package[$glance::params::registry_package_name] -> Glance_registry_config<||> + + Glance_registry_config<||> ~> Exec<| title == 'glance-manage db_sync' |> + Glance_registry_config<||> ~> Service['glance-registry'] + + File { + ensure => present, + owner => 'glance', + group => 'glance', + mode => '0640', + notify => Service['glance-registry'], + require => Class['glance'] + } + + if $sql_connection { + warning('The sql_connection parameter is deprecated, use database_connection instead.') + $database_connection_real = $sql_connection + } else { + $database_connection_real = $database_connection + } + + if $sql_idle_timeout { + warning('The sql_idle_timeout parameter is deprecated, use database_idle_timeout instead.') + $database_idle_timeout_real = $sql_idle_timeout + } else { + $database_idle_timeout_real = $database_idle_timeout + } + + if $database_connection_real { + if($database_connection_real =~ /mysql:\/\/\S+:\S+@\S+\/\S+/) { + require 'mysql::bindings' + require 'mysql::bindings::python' + } elsif($database_connection_real =~ /postgresql:\/\/\S+:\S+@\S+\/\S+/) { + + } elsif($database_connection_real =~ /sqlite:\/\//) { + + } else { + fail("Invalid db connection ${database_connection_real}") + } + glance_registry_config { + 'database/connection': value => $database_connection_real, secret => true; + 'database/idle_timeout': value => $database_idle_timeout_real; + } + } + + glance_registry_config { + 'DEFAULT/verbose': value => $verbose; + 'DEFAULT/debug': value => $debug; + 'DEFAULT/bind_host': value => $bind_host; + 'DEFAULT/bind_port': value => $bind_port; + } + + if $auth_uri { + glance_registry_config { 'keystone_authtoken/auth_uri': value => $auth_uri; } + } else { + glance_registry_config { 'keystone_authtoken/auth_uri': value => "${auth_protocol}://${auth_host}:5000/"; } + } + + # auth config + glance_registry_config { + 'keystone_authtoken/auth_host': value => $auth_host; + 'keystone_authtoken/auth_port': value => $auth_port; + 'keystone_authtoken/auth_protocol': value => $auth_protocol; + } + + if $auth_admin_prefix { + validate_re($auth_admin_prefix, '^(/.+[^/])?$') + glance_registry_config { + 'keystone_authtoken/auth_admin_prefix': value => $auth_admin_prefix; + } + } else { + glance_registry_config { + 'keystone_authtoken/auth_admin_prefix': ensure => absent; + } + } + + # Set the pipeline, it is allowed to be blank + if $pipeline != '' { + validate_re($pipeline, '^(\w+([+]\w+)*)*$') + glance_registry_config { + 'paste_deploy/flavor': + ensure => present, + value => $pipeline, + } + } else { + glance_registry_config { 'paste_deploy/flavor': ensure => absent } + } + + # keystone config + if $auth_type == 'keystone' { + glance_registry_config { + 'keystone_authtoken/admin_tenant_name': value => $keystone_tenant; + 'keystone_authtoken/admin_user' : value => $keystone_user; + 'keystone_authtoken/admin_password' : value => $keystone_password, secret => true; + } + } + + # SSL Options + if $cert_file { + glance_registry_config { + 'DEFAULT/cert_file' : value => $cert_file; + } + } else { + glance_registry_config { + 'DEFAULT/cert_file': ensure => absent; + } + } + if $key_file { + glance_registry_config { + 'DEFAULT/key_file' : value => $key_file; + } + } else { + glance_registry_config { + 'DEFAULT/key_file': ensure => absent; + } + } + if $ca_file { + glance_registry_config { + 'DEFAULT/ca_file' : value => $ca_file; + } + } else { + glance_registry_config { + 'DEFAULT/ca_file': ensure => absent; + } + } + + # Logging + if $log_file { + glance_registry_config { + 'DEFAULT/log_file': value => $log_file; + } + } else { + glance_registry_config { + 'DEFAULT/log_file': ensure => absent; + } + } + + if $log_dir { + glance_registry_config { + 'DEFAULT/log_dir': value => $log_dir; + } + } else { + glance_registry_config { + 'DEFAULT/log_dir': ensure => absent; + } + } + + # Syslog + if $use_syslog { + glance_registry_config { + 'DEFAULT/use_syslog': value => true; + 'DEFAULT/syslog_log_facility': value => $log_facility; + } + } else { + glance_registry_config { + 'DEFAULT/use_syslog': value => false; + } + } + + resources { 'glance_registry_config': + purge => $purge_config + } + + file { ['/etc/glance/glance-registry.conf', + '/etc/glance/glance-registry-paste.ini']: + } + + + if $manage_service { + if $enabled { + Exec['glance-manage db_sync'] ~> Service['glance-registry'] + + exec { 'glance-manage db_sync': + command => $::glance::params::db_sync_command, + path => '/usr/bin', + user => 'glance', + refreshonly => true, + logoutput => on_failure, + subscribe => [Package[$glance::params::registry_package_name], File['/etc/glance/glance-registry.conf']], + } + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + service { 'glance-registry': + ensure => $service_ensure, + name => $::glance::params::registry_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + subscribe => File['/etc/glance/glance-registry.conf'], + require => Class['glance'] + } + +} diff --git a/glance/spec/classes/glance_api_spec.rb b/glance/spec/classes/glance_api_spec.rb new file mode 100644 index 000000000..806700ded --- /dev/null +++ b/glance/spec/classes/glance_api_spec.rb @@ -0,0 +1,378 @@ +require 'spec_helper' + +describe 'glance::api' do + + let :facts do + { + :osfamily => 'Debian', + :processorcount => '7', + } + end + + let :default_params do + { + :verbose => false, + :debug => false, + :bind_host => '0.0.0.0', + :bind_port => '9292', + :registry_host => '0.0.0.0', + :registry_port => '9191', + :registry_client_protocol => 'http', + :log_file => '/var/log/glance/api.log', + :log_dir => '/var/log/glance', + :auth_type => 'keystone', + :enabled => true, + :manage_service => true, + :backlog => '4096', + :workers => '7', + :auth_host => '127.0.0.1', + :auth_port => '35357', + :auth_protocol => 'http', + :auth_uri => 'http://127.0.0.1:5000/', + :keystone_tenant => 'services', + :keystone_user => 'glance', + :keystone_password => 'ChangeMe', + :database_idle_timeout => '3600', + :database_connection => 'sqlite:///var/lib/glance/glance.sqlite', + :show_image_direct_url => false, + :purge_config => false, + :known_stores => false, + :image_cache_dir => '/var/lib/glance/image-cache', + } + end + + [{:keystone_password => 'ChangeMe'}, + { + :verbose => true, + :debug => true, + :bind_host => '127.0.0.1', + :bind_port => '9222', + :registry_host => '127.0.0.1', + :registry_port => '9111', + :registry_client_protocol => 'https', + :auth_type => 'not_keystone', + :enabled => false, + :backlog => '4095', + :workers => '5', + :auth_host => '127.0.0.2', + :auth_port => '35358', + :auth_protocol => 'https', + :auth_uri => 'https://127.0.0.2:5000/v2.0/', + :keystone_tenant => 'admin2', + :keystone_user => 'admin2', + :keystone_password => 'ChangeMe2', + :database_idle_timeout => '36002', + :database_connection => 'mysql:///var:lib@glance/glance', + :show_image_direct_url => true, + :image_cache_dir => '/tmp/glance' + } + ].each do |param_set| + + describe "when #{param_set == {:keystone_password => 'ChangeMe'} ? "using default" : "specifying"} class parameters" do + + let :param_hash do + default_params.merge(param_set) + end + + let :params do + param_set + end + + it { should contain_class 'glance' } + + it { should contain_service('glance-api').with( + 'ensure' => (param_hash[:manage_service] && param_hash[:enabled]) ? 'running': 'stopped', + 'enable' => param_hash[:enabled], + 'hasstatus' => true, + 'hasrestart' => true + ) } + + it 'should lay down default api config' do + [ + 'verbose', + 'debug', + 'bind_host', + 'bind_port', + 'registry_host', + 'registry_port', + 'registry_client_protocol', + 'show_image_direct_url' + ].each do |config| + should contain_glance_api_config("DEFAULT/#{config}").with_value(param_hash[config.intern]) + end + end + + it 'should lay down default cache config' do + [ + 'verbose', + 'debug', + 'registry_host', + 'registry_port' + ].each do |config| + should contain_glance_cache_config("DEFAULT/#{config}").with_value(param_hash[config.intern]) + end + end + + it 'should config db' do + should contain_glance_api_config('database/connection').with_value(param_hash[:database_connection]) + should contain_glance_api_config('database/connection').with_value(param_hash[:database_connection]).with_secret(true) + should contain_glance_api_config('database/idle_timeout').with_value(param_hash[:database_idle_timeout]) + end + + it 'should have no ssl options' do + should contain_glance_api_config('DEFAULT/ca_file').with_ensure('absent') + should contain_glance_api_config('DEFAULT/cert_file').with_ensure('absent') + should contain_glance_api_config('DEFAULT/key_file').with_ensure('absent') + end + + it 'should lay down default auth config' do + [ + 'auth_host', + 'auth_port', + 'auth_protocol' + ].each do |config| + should contain_glance_api_config("keystone_authtoken/#{config}").with_value(param_hash[config.intern]) + end + end + it { should contain_glance_api_config('keystone_authtoken/auth_admin_prefix').with_ensure('absent') } + + it 'should configure itself for keystone if that is the auth_type' do + if params[:auth_type] == 'keystone' + should contain('paste_deploy/flavor').with_value('keystone+cachemanagement') + + ['admin_tenant_name', 'admin_user', 'admin_password'].each do |config| + should contain_glance_api_config("keystone_authtoken/#{config}").with_value(param_hash[config.intern]) + end + should contain_glance_api_config('keystone_authtoken/admin_password').with_value(param_hash[:keystone_password]).with_secret(true) + + ['admin_tenant_name', 'admin_user', 'admin_password'].each do |config| + should contain_glance_cache_config("keystone_authtoken/#{config}").with_value(param_hash[config.intern]) + end + should contain_glance_cache_config('keystone_authtoken/admin_password').with_value(param_hash[:keystone_password]).with_secret(true) + end + end + end + end + + describe 'with disabled service managing' do + let :params do + { + :keystone_password => 'ChangeMe', + :manage_service => false, + :enabled => false, + } + end + + it { should contain_service('glance-api').with( + 'ensure' => nil, + 'enable' => false, + 'hasstatus' => true, + 'hasrestart' => true + ) } + end + + describe 'with overridden pipeline' do + let :params do + { + :keystone_password => 'ChangeMe', + :pipeline => 'keystone', + } + end + + it { should contain_glance_api_config('paste_deploy/flavor').with_value('keystone') } + end + + describe 'with blank pipeline' do + let :params do + { + :keystone_password => 'ChangeMe', + :pipeline => '', + } + end + + it { should contain_glance_api_config('paste_deploy/flavor').with_ensure('absent') } + end + + [ + 'keystone/', + 'keystone+', + '+keystone', + 'keystone+cachemanagement+', + '+' + ].each do |pipeline| + describe "with pipeline incorrect value #{pipeline}" do + let :params do + { + :keystone_password => 'ChangeMe', + :pipeline => pipeline + } + end + + it { expect { should contain_glance_api_config('filter:paste_deploy/flavor') }.to\ + raise_error(Puppet::Error, /validate_re\(\): .* does not match/) } + end + end + + describe 'with overriden auth_admin_prefix' do + let :params do + { + :keystone_password => 'ChangeMe', + :auth_admin_prefix => '/keystone/main' + } + end + + it { should contain_glance_api_config('keystone_authtoken/auth_admin_prefix').with_value('/keystone/main') } + end + + [ + '/keystone/', + 'keystone/', + 'keystone', + '/keystone/admin/', + 'keystone/admin/', + 'keystone/admin' + ].each do |auth_admin_prefix| + describe "with auth_admin_prefix_containing incorrect value #{auth_admin_prefix}" do + let :params do + { + :keystone_password => 'ChangeMe', + :auth_admin_prefix => auth_admin_prefix + } + end + + it { expect { should contain_glance_api_config('filter:authtoken/auth_admin_prefix') }.to\ + raise_error(Puppet::Error, /validate_re\(\): "#{auth_admin_prefix}" does not match/) } + end + end + + describe 'with syslog disabled by default' do + let :params do + default_params + end + + it { should contain_glance_api_config('DEFAULT/use_syslog').with_value(false) } + it { should_not contain_glance_api_config('DEFAULT/syslog_log_facility') } + end + + describe 'with syslog enabled' do + let :params do + default_params.merge({ + :use_syslog => 'true', + }) + end + + it { should contain_glance_api_config('DEFAULT/use_syslog').with_value(true) } + it { should contain_glance_api_config('DEFAULT/syslog_log_facility').with_value('LOG_USER') } + end + + describe 'with syslog enabled and custom settings' do + let :params do + default_params.merge({ + :use_syslog => 'true', + :log_facility => 'LOG_LOCAL0' + }) + end + + it { should contain_glance_api_config('DEFAULT/use_syslog').with_value(true) } + it { should contain_glance_api_config('DEFAULT/syslog_log_facility').with_value('LOG_LOCAL0') } + end + + describe 'with log_file enabled by default' do + let(:params) { default_params } + + it { should contain_glance_api_config('DEFAULT/log_file').with_value(default_params[:log_file]) } + + context 'with log_file disabled' do + let(:params) { default_params.merge!({ :log_file => false }) } + it { should contain_glance_api_config('DEFAULT/log_file').with_ensure('absent') } + end + end + + describe 'with log_dir enabled by default' do + let(:params) { default_params } + + it { should contain_glance_api_config('DEFAULT/log_dir').with_value(default_params[:log_dir]) } + + context 'with log_dir disabled' do + let(:params) { default_params.merge!({ :log_dir => false }) } + it { should contain_glance_api_config('DEFAULT/log_dir').with_ensure('absent') } + end + end + + describe 'with ssl options' do + let :params do + default_params.merge({ + :ca_file => '/tmp/ca_file', + :cert_file => '/tmp/cert_file', + :key_file => '/tmp/key_file' + }) + end + + context 'with ssl options' do + it { should contain_glance_api_config('DEFAULT/ca_file').with_value('/tmp/ca_file') } + it { should contain_glance_api_config('DEFAULT/cert_file').with_value('/tmp/cert_file') } + it { should contain_glance_api_config('DEFAULT/key_file').with_value('/tmp/key_file') } + end + end + describe 'with known_stores by default' do + let :params do + default_params + end + + it { should_not contain_glance_api_config('DEFAULT/known_stores').with_value('false') } + end + + describe 'with known_stores override' do + let :params do + default_params.merge({ + :known_stores => ['glance.store.filesystem.Store','glance.store.http.Store'], + }) + end + + it { should contain_glance_api_config('DEFAULT/known_stores').with_value("glance.store.filesystem.Store,glance.store.http.Store") } + end + + describe 'with deprecated sql parameters' do + let :params do + default_params.merge({ + :sql_connection => 'mysql://user:pass@db/db', + :sql_idle_timeout => '30' + }) + end + + it 'configures database' do + should contain_glance_api_config('database/connection').with_value('mysql://user:pass@db/db') + should contain_glance_api_config('database/idle_timeout').with_value('30') + end + end + + describe 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + let(:params) { default_params } + + it {should contain_package('glance-api')} + end + + describe 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + let(:params) { default_params } + + it { should contain_package('openstack-glance')} + end + + describe 'on unknown platforms' do + let :facts do + { :osfamily => 'unknown' } + end + let(:params) { default_params } + + it 'should fails to configure glance-api' do + expect { subject }.to raise_error(Puppet::Error, /module glance only support osfamily RedHat and Debian/) + end + end + +end diff --git a/glance/spec/classes/glance_backend_cinder_spec.rb b/glance/spec/classes/glance_backend_cinder_spec.rb new file mode 100644 index 000000000..f3f33ac63 --- /dev/null +++ b/glance/spec/classes/glance_backend_cinder_spec.rb @@ -0,0 +1,99 @@ +# +# Copyright (C) 2013 eNovance SAS +# +# Author: Emilien Macchi +# +# 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. +# +# Unit tests for glance::backend::cinder class +# + +require 'spec_helper' + +describe 'glance::backend::cinder' do + + let :pre_condition do + 'class { "glance::api": keystone_password => "pass" }' + end + + shared_examples_for 'glance with cinder backend' do + + context 'when default parameters' do + + it 'configures glance-api.conf' do + should contain_glance_api_config('DEFAULT/default_store').with_value('cinder') + should contain_glance_api_config('DEFAULT/cinder_api_insecure').with_value(false) + should contain_glance_api_config('DEFAULT/cinder_catalog_info').with_value('volume:cinder:publicURL') + should contain_glance_api_config('DEFAULT/os_region_name').with_value('RegionOne') + should contain_glance_api_config('DEFAULT/cinder_http_retries').with_value('3') + should contain_glance_api_config('DEFAULT/cinder_ca_certificates_file').with(:ensure => 'absent') + should contain_glance_api_config('DEFAULT/cinder_endpoint_template').with(:ensure => 'absent') + end + it 'configures glance-cache.conf' do + should contain_glance_cache_config('DEFAULT/cinder_api_insecure').with_value(false) + should contain_glance_cache_config('DEFAULT/cinder_catalog_info').with_value('volume:cinder:publicURL') + should contain_glance_cache_config('DEFAULT/os_region_name').with_value('RegionOne') + should contain_glance_cache_config('DEFAULT/cinder_http_retries').with_value('3') + should contain_glance_cache_config('DEFAULT/cinder_ca_certificates_file').with(:ensure => 'absent') + should contain_glance_cache_config('DEFAULT/cinder_endpoint_template').with(:ensure => 'absent') + end + end + + context 'when overriding parameters' do + let :params do + { + :cinder_api_insecure => true, + :cinder_ca_certificates_file => '/etc/ssh/ca.crt', + :cinder_catalog_info => 'volume:cinder:internalURL', + :cinder_endpoint_template => 'http://srv-foo:8776/v1/%(project_id)s', + :cinder_http_retries => '10', + :os_region_name => 'foo' + } + end + it 'configures glance-api.conf' do + should contain_glance_api_config('DEFAULT/default_store').with_value('cinder') + should contain_glance_api_config('DEFAULT/cinder_api_insecure').with_value(true) + should contain_glance_api_config('DEFAULT/cinder_ca_certificates_file').with_value('/etc/ssh/ca.crt') + should contain_glance_api_config('DEFAULT/cinder_catalog_info').with_value('volume:cinder:internalURL') + should contain_glance_api_config('DEFAULT/cinder_endpoint_template').with_value('http://srv-foo:8776/v1/%(project_id)s') + should contain_glance_api_config('DEFAULT/cinder_http_retries').with_value('10') + should contain_glance_api_config('DEFAULT/os_region_name').with_value('foo') + end + it 'configures glance-cache.conf' do + should contain_glance_cache_config('DEFAULT/cinder_api_insecure').with_value(true) + should contain_glance_cache_config('DEFAULT/cinder_ca_certificates_file').with_value('/etc/ssh/ca.crt') + should contain_glance_cache_config('DEFAULT/cinder_catalog_info').with_value('volume:cinder:internalURL') + should contain_glance_cache_config('DEFAULT/cinder_endpoint_template').with_value('http://srv-foo:8776/v1/%(project_id)s') + should contain_glance_cache_config('DEFAULT/cinder_http_retries').with_value('10') + should contain_glance_cache_config('DEFAULT/os_region_name').with_value('foo') + end + end + + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'glance with cinder backend' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'glance with cinder backend' + end +end diff --git a/glance/spec/classes/glance_backend_file_spec.rb b/glance/spec/classes/glance_backend_file_spec.rb new file mode 100644 index 000000000..fac7fcf46 --- /dev/null +++ b/glance/spec/classes/glance_backend_file_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe 'glance::backend::file' do + let :facts do + { :osfamily => 'Debian' } + end + + let :pre_condition do + 'class { "glance::api": keystone_password => "pass" }' + end + + it 'configures glance-api.conf' do + should contain_glance_api_config('DEFAULT/default_store').with_value('file') + should contain_glance_api_config('DEFAULT/filesystem_store_datadir').with_value('/var/lib/glance/images/') + end + + it 'configures glance-cache.conf' do + should contain_glance_cache_config('DEFAULT/filesystem_store_datadir').with_value('/var/lib/glance/images/') + end + + describe 'when overriding datadir' do + let :params do + {:filesystem_store_datadir => '/tmp/'} + end + + it 'configures glance-api.conf' do + should contain_glance_api_config('DEFAULT/filesystem_store_datadir').with_value('/tmp/') + end + + it 'configures glance-cache.conf' do + should contain_glance_cache_config('DEFAULT/filesystem_store_datadir').with_value('/tmp/') + end + end +end diff --git a/glance/spec/classes/glance_backend_rbd_spec.rb b/glance/spec/classes/glance_backend_rbd_spec.rb new file mode 100644 index 000000000..f78b61e83 --- /dev/null +++ b/glance/spec/classes/glance_backend_rbd_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe 'glance::backend::rbd' do + let :facts do + { + :osfamily => 'Debian' + } + end + + describe 'when defaults with rbd_store_user' do + let :params do + { + :rbd_store_user => 'glance', + } + end + + it { should contain_glance_api_config('DEFAULT/default_store').with_value('rbd') } + it { should contain_glance_api_config('DEFAULT/rbd_store_pool').with_value('images') } + it { should contain_glance_api_config('DEFAULT/rbd_store_ceph_conf').with_value('/etc/ceph/ceph.conf') } + it { should contain_glance_api_config('DEFAULT/rbd_store_chunk_size').with_value('8') } + + it { should contain_package('python-ceph').with( + :name => 'python-ceph', + :ensure => 'present' + ) + } + end + + describe 'when passing params' do + let :params do + { + :rbd_store_user => 'user', + :rbd_store_chunk_size => '2', + :package_ensure => 'latest', + } + end + it { should contain_glance_api_config('DEFAULT/rbd_store_user').with_value('user') } + it { should contain_glance_api_config('DEFAULT/rbd_store_chunk_size').with_value('2') } + it { should contain_package('python-ceph').with( + :name => 'python-ceph', + :ensure => 'latest' + ) + } + end +end diff --git a/glance/spec/classes/glance_backend_swift_spec.rb b/glance/spec/classes/glance_backend_swift_spec.rb new file mode 100644 index 000000000..0c5569a90 --- /dev/null +++ b/glance/spec/classes/glance_backend_swift_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe 'glance::backend::swift' do + let :facts do + { + :osfamily => 'Debian' + } + end + + let :params do + { + :swift_store_user => 'user', + :swift_store_key => 'key', + } + end + + let :pre_condition do + 'class { "glance::api": keystone_password => "pass" }' + end + + describe 'when default parameters' do + + it 'configures glance-api.conf' do + should contain_glance_api_config('DEFAULT/default_store').with_value('swift') + should contain_glance_api_config('DEFAULT/swift_store_key').with_value('key') + should contain_glance_api_config('DEFAULT/swift_store_user').with_value('user') + should contain_glance_api_config('DEFAULT/swift_store_auth_version').with_value('2') + should contain_glance_api_config('DEFAULT/swift_store_large_object_size').with_value('5120') + should contain_glance_api_config('DEFAULT/swift_store_auth_address').with_value('127.0.0.1:5000/v2.0/') + should contain_glance_api_config('DEFAULT/swift_store_container').with_value('glance') + should contain_glance_api_config('DEFAULT/swift_store_create_container_on_put').with_value(false) + end + + it 'configures glance-cache.conf' do + should contain_glance_cache_config('DEFAULT/swift_store_key').with_value('key') + should contain_glance_cache_config('DEFAULT/swift_store_user').with_value('user') + should contain_glance_cache_config('DEFAULT/swift_store_auth_version').with_value('2') + should contain_glance_cache_config('DEFAULT/swift_store_large_object_size').with_value('5120') + should contain_glance_cache_config('DEFAULT/swift_store_auth_address').with_value('127.0.0.1:5000/v2.0/') + should contain_glance_cache_config('DEFAULT/swift_store_container').with_value('glance') + should contain_glance_cache_config('DEFAULT/swift_store_create_container_on_put').with_value(false) + end + end + + describe 'when overriding parameters' do + let :params do + { + :swift_store_user => 'user', + :swift_store_key => 'key', + :swift_store_auth_version => '1', + :swift_store_large_object_size => '100', + :swift_store_auth_address => '127.0.0.2:8080/v1.0/', + :swift_store_container => 'swift', + :swift_store_create_container_on_put => true + } + end + + it 'configures glance-api.conf' do + should contain_glance_api_config('DEFAULT/swift_store_container').with_value('swift') + should contain_glance_api_config('DEFAULT/swift_store_create_container_on_put').with_value(true) + should contain_glance_api_config('DEFAULT/swift_store_auth_version').with_value('1') + should contain_glance_api_config('DEFAULT/swift_store_large_object_size').with_value('100') + should contain_glance_api_config('DEFAULT/swift_store_auth_address').with_value('127.0.0.2:8080/v1.0/') + end + + it 'configures glance-cache.conf' do + should contain_glance_cache_config('DEFAULT/swift_store_container').with_value('swift') + should contain_glance_cache_config('DEFAULT/swift_store_create_container_on_put').with_value(true) + should contain_glance_cache_config('DEFAULT/swift_store_auth_version').with_value('1') + should contain_glance_cache_config('DEFAULT/swift_store_large_object_size').with_value('100') + should contain_glance_cache_config('DEFAULT/swift_store_auth_address').with_value('127.0.0.2:8080/v1.0/') + end + end +end diff --git a/glance/spec/classes/glance_cache_cleaner_spec.rb b/glance/spec/classes/glance_cache_cleaner_spec.rb new file mode 100644 index 000000000..a5bb314c9 --- /dev/null +++ b/glance/spec/classes/glance_cache_cleaner_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe 'glance::cache::cleaner' do + + shared_examples_for 'glance cache cleaner' do + + context 'when default parameters' do + + it 'configures a cron' do + should contain_cron('glance-cache-cleaner').with( + :command => 'glance-cache-cleaner ', + :environment => 'PATH=/bin:/usr/bin:/usr/sbin', + :user => 'glance', + :minute => 1, + :hour => 0, + :monthday => '*', + :month => '*', + :weekday => '*' + ) + end + end + + context 'when overriding parameters' do + let :params do + { + :minute => 59, + :hour => 23, + :monthday => '1', + :month => '2', + :weekday => '3', + :command_options => '--config-dir /etc/glance/', + } + end + it 'configures a cron' do + should contain_cron('glance-cache-cleaner').with( + :command => 'glance-cache-cleaner --config-dir /etc/glance/', + :environment => 'PATH=/bin:/usr/bin:/usr/sbin', + :user => 'glance', + :minute => 59, + :hour => 23, + :monthday => '1', + :month => '2', + :weekday => '3' + ) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + include_examples 'glance cache cleaner' + it { should contain_cron('glance-cache-cleaner').with(:require => 'Package[glance-api]')} + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + include_examples 'glance cache cleaner' + it { should contain_cron('glance-cache-cleaner').with(:require => 'Package[openstack-glance]')} + end + +end diff --git a/glance/spec/classes/glance_cache_pruner_spec.rb b/glance/spec/classes/glance_cache_pruner_spec.rb new file mode 100644 index 000000000..87bb46ca7 --- /dev/null +++ b/glance/spec/classes/glance_cache_pruner_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe 'glance::cache::pruner' do + + shared_examples_for 'glance cache pruner' do + + context 'when default parameters' do + + it 'configures a cron' do + should contain_cron('glance-cache-pruner').with( + :command => 'glance-cache-pruner ', + :environment => 'PATH=/bin:/usr/bin:/usr/sbin', + :user => 'glance', + :minute => '*/30', + :hour => '*', + :monthday => '*', + :month => '*', + :weekday => '*' + ) + end + end + + context 'when overriding parameters' do + let :params do + { + :minute => 59, + :hour => 23, + :monthday => '1', + :month => '2', + :weekday => '3', + :command_options => '--config-dir /etc/glance/', + } + end + it 'configures a cron' do + should contain_cron('glance-cache-pruner').with( + :command => 'glance-cache-pruner --config-dir /etc/glance/', + :environment => 'PATH=/bin:/usr/bin:/usr/sbin', + :user => 'glance', + :minute => 59, + :hour => 23, + :monthday => '1', + :month => '2', + :weekday => '3' + ) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + include_examples 'glance cache pruner' + it { should contain_cron('glance-cache-pruner').with(:require => 'Package[glance-api]')} + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + include_examples 'glance cache pruner' + it { should contain_cron('glance-cache-pruner').with(:require => 'Package[openstack-glance]')} + end + +end diff --git a/glance/spec/classes/glance_client_spec.rb b/glance/spec/classes/glance_client_spec.rb new file mode 100644 index 000000000..e00e29a93 --- /dev/null +++ b/glance/spec/classes/glance_client_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe 'glance::client' do + + shared_examples 'glance client' do + it { should contain_class('glance::params') } + it { should contain_package('python-glanceclient').with( + :name => 'python-glanceclient', + :ensure => 'present' + ) + } + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + include_examples 'glance client' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + include_examples 'glance client' + end +end diff --git a/glance/spec/classes/glance_db_mysql_spec.rb b/glance/spec/classes/glance_db_mysql_spec.rb new file mode 100644 index 000000000..ed14a9bbf --- /dev/null +++ b/glance/spec/classes/glance_db_mysql_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe 'glance::db::mysql' do + let :facts do + { + :osfamily => 'Debian' + } + end + + let :pre_condition do + 'include mysql::server' + end + + describe "with default params" do + let :params do + { + :password => 'glancepass1', + } + end + + it { should contain_openstacklib__db__mysql('glance').with( + :password_hash => '*41C910F70EB213CF4CB7B2F561B4995503C0A87B', + :charset => 'utf8' + )} + + end + + describe "overriding default params" do + let :params do + { + :password => 'glancepass2', + :dbname => 'glancedb2', + :charset => 'utf8', + } + end + + it { should contain_openstacklib__db__mysql('glance').with( + :password_hash => '*6F9A1CB9BD83EE06F3903BDFF9F4188764E694CA', + :dbname => 'glancedb2', + :charset => 'utf8' + )} + + end + + describe "overriding allowed_hosts param to array" do + let :params do + { + :password => 'glancepass2', + :dbname => 'glancedb2', + :allowed_hosts => ['127.0.0.1','%'] + } + end + + end + + describe "overriding allowed_hosts param to string" do + let :params do + { + :password => 'glancepass2', + :dbname => 'glancedb2', + :allowed_hosts => '192.168.1.1' + } + end + + end + + describe "overriding allowed_hosts param equals to host param " do + let :params do + { + :password => 'glancepass2', + :dbname => 'glancedb2', + :allowed_hosts => '127.0.0.1' + } + end + + end + +end diff --git a/glance/spec/classes/glance_db_postgresql_spec.rb b/glance/spec/classes/glance_db_postgresql_spec.rb new file mode 100644 index 000000000..dcb2b0126 --- /dev/null +++ b/glance/spec/classes/glance_db_postgresql_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'glance::db::postgresql' do + + let :req_params do + {:password => 'pw'} + end + + let :facts do + { + :postgres_default_version => '8.4', + :osfamily => 'RedHat', + } + end + + describe 'with only required params' do + let :params do + req_params + end + it { should contain_postgresql__db('glance').with( + :user => 'glance', + :password => 'pw' + ) } + end + +end diff --git a/glance/spec/classes/glance_keystone_auth_spec.rb b/glance/spec/classes/glance_keystone_auth_spec.rb new file mode 100644 index 000000000..79de6b680 --- /dev/null +++ b/glance/spec/classes/glance_keystone_auth_spec.rb @@ -0,0 +1,175 @@ +require 'spec_helper' + +describe 'glance::keystone::auth' do + + describe 'with defaults' do + + let :params do + {:password => 'pass'} + end + + it { should contain_keystone_user('glance').with( + :ensure => 'present', + :password => 'pass' + )} + + it { should contain_keystone_user_role('glance@services').with( + :ensure => 'present', + :roles => 'admin' + ) } + + it { should contain_keystone_service('glance').with( + :ensure => 'present', + :type => 'image', + :description => 'Openstack Image Service' + ) } + + it { should contain_keystone_endpoint('RegionOne/glance').with( + :ensure => 'present', + :public_url => 'http://127.0.0.1:9292', + :admin_url => 'http://127.0.0.1:9292', + :internal_url => 'http://127.0.0.1:9292' + )} + + end + + describe 'when auth_type, password, and service_type are overridden' do + + let :params do + { + :auth_name => 'glancey', + :password => 'password', + :service_type => 'imagey' + } + end + + it { should contain_keystone_user('glancey').with( + :ensure => 'present', + :password => 'password' + )} + + it { should contain_keystone_user_role('glancey@services').with( + :ensure => 'present', + :roles => 'admin' + ) } + + it { should contain_keystone_service('glancey').with( + :ensure => 'present', + :type => 'imagey', + :description => 'Openstack Image Service' + ) } + + end + + describe 'when address, region, port and protocoll are overridden' do + + let :params do + { + :password => 'pass', + :public_address => '10.0.0.1', + :admin_address => '10.0.0.2', + :internal_address => '10.0.0.3', + :port => '9393', + :region => 'RegionTwo', + :public_protocol => 'https', + :admin_protocol => 'https', + :internal_protocol => 'https' + } + end + + it { should contain_keystone_endpoint('RegionTwo/glance').with( + :ensure => 'present', + :public_url => 'https://10.0.0.1:9393', + :admin_url => 'https://10.0.0.2:9393', + :internal_url => 'https://10.0.0.3:9393' + )} + + end + + describe 'when endpoint is not set' do + + let :params do + { + :configure_endpoint => false, + :password => 'pass', + } + end + + it { should_not contain_keystone_endpoint('glance') } + end + + describe 'when disabling user configuration' do + let :params do + { + :configure_user => false, + :password => 'pass', + } + end + + it { should_not contain_keystone_user('glance') } + + it { should contain_keystone_user_role('glance@services') } + + it { should contain_keystone_service('glance').with( + :ensure => 'present', + :type => 'image', + :description => 'Openstack Image Service' + ) } + end + + describe 'when disabling user and user role configuration' do + let :params do + { + :configure_user => false, + :configure_user_role => false, + :password => 'pass', + } + end + + it { should_not contain_keystone_user('glance') } + + it { should_not contain_keystone_user_role('glance@services') } + + it { should contain_keystone_service('glance').with( + :ensure => 'present', + :type => 'image', + :description => 'Openstack Image Service' + ) } + end + + describe 'when configuring glance-api and the keystone endpoint' do + let :pre_condition do + "class { 'glance::api': keystone_password => 'test' }" + end + + let :facts do + { :osfamily => 'Debian' } + end + + let :params do + { + :password => 'test', + :configure_endpoint => true + } + end + + it { should contain_keystone_endpoint('RegionOne/glance').with_notify('Service[glance-api]') } + end + + describe 'when overriding service name' do + + let :params do + { + :service_name => 'glance_service', + :password => 'pass' + } + end + + it { should contain_keystone_user('glance') } + it { should contain_keystone_user_role('glance@services') } + it { should contain_keystone_service('glance_service') } + it { should contain_keystone_endpoint('RegionOne/glance_service') } + + end + +end diff --git a/glance/spec/classes/glance_notify_qpid_spec.rb b/glance/spec/classes/glance_notify_qpid_spec.rb new file mode 100644 index 000000000..f6c615835 --- /dev/null +++ b/glance/spec/classes/glance_notify_qpid_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' +describe 'glance::notify::qpid' do + let :facts do + { + :osfamily => 'Debian' + } + end + + let :pre_condition do + 'class { "glance::api": keystone_password => "pass" }' + end + + describe 'when default params and qpid_password' do + let :params do + {:qpid_password => 'pass'} + end + + it { should contain_glance_api_config('DEFAULT/notifier_driver').with_value('qpid') } + it { should contain_glance_api_config('DEFAULT/qpid_username').with_value('guest') } + it { should contain_glance_api_config('DEFAULT/qpid_password').with_value('pass') } + it { should contain_glance_api_config('DEFAULT/qpid_password').with_value(params[:qpid_password]).with_secret(true) } + it { should contain_glance_api_config('DEFAULT/qpid_hostname').with_value('localhost') } + it { should contain_glance_api_config('DEFAULT/qpid_port').with_value('5672') } + it { should contain_glance_api_config('DEFAULT/qpid_protocol').with_value('tcp') } + end + + describe 'when passing params' do + let :params do + { + :qpid_password => 'pass2', + :qpid_username => 'guest2', + :qpid_hostname => 'localhost2', + :qpid_port => '5673' + } + end + it { should contain_glance_api_config('DEFAULT/qpid_username').with_value('guest2') } + it { should contain_glance_api_config('DEFAULT/qpid_hostname').with_value('localhost2') } + it { should contain_glance_api_config('DEFAULT/qpid_port').with_value('5673') } + it { should contain_glance_api_config('DEFAULT/qpid_protocol').with_value('tcp') } + end + + describe 'when configuring with ssl' do + let :params do + { + :qpid_password => 'pass3', + :qpid_username => 'guest3', + :qpid_hostname => 'localhost3', + :qpid_port => '5671', + :qpid_protocol => 'ssl' + } + end + it { should contain_glance_api_config('DEFAULT/qpid_username').with_value('guest3') } + it { should contain_glance_api_config('DEFAULT/qpid_hostname').with_value('localhost3') } + it { should contain_glance_api_config('DEFAULT/qpid_port').with_value('5671') } + it { should contain_glance_api_config('DEFAULT/qpid_protocol').with_value('ssl') } + end +end diff --git a/glance/spec/classes/glance_notify_rabbitmq_spec.rb b/glance/spec/classes/glance_notify_rabbitmq_spec.rb new file mode 100644 index 000000000..47163fa04 --- /dev/null +++ b/glance/spec/classes/glance_notify_rabbitmq_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' +describe 'glance::notify::rabbitmq' do + let :facts do + { + :osfamily => 'Debian' + } + end + + let :pre_condition do + 'class { "glance::api": keystone_password => "pass" }' + end + + describe 'when defaults with rabbit pass specified' do + let :params do + {:rabbit_password => 'pass'} + end + it { should contain_glance_api_config('DEFAULT/notification_driver').with_value('messaging') } + it { should contain_glance_api_config('DEFAULT/rabbit_password').with_value('pass') } + it { should contain_glance_api_config('DEFAULT/rabbit_password').with_value(params[:rabbit_password]).with_secret(true) } + it { should contain_glance_api_config('DEFAULT/rabbit_userid').with_value('guest') } + it { should contain_glance_api_config('DEFAULT/rabbit_host').with_value('localhost') } + it { should contain_glance_api_config('DEFAULT/rabbit_port').with_value('5672') } + it { should contain_glance_api_config('DEFAULT/rabbit_hosts').with_value('localhost:5672') } + it { should contain_glance_api_config('DEFAULT/rabbit_ha_queues').with_value('false') } + it { should contain_glance_api_config('DEFAULT/amqp_durable_queues').with_value('false') } + it { should contain_glance_api_config('DEFAULT/rabbit_virtual_host').with_value('/') } + it { should contain_glance_api_config('DEFAULT/rabbit_notification_exchange').with_value('glance') } + it { should contain_glance_api_config('DEFAULT/rabbit_notification_topic').with_value('notifications') } + end + + describe 'when passing params' do + let :params do + { + :rabbit_password => 'pass', + :rabbit_userid => 'guest2', + :rabbit_host => 'localhost2', + :rabbit_port => '5673', + :rabbit_durable_queues => true, + } + it { should contain_glance_api_config('DEFAULT/rabbit_userid').with_value('guest2') } + it { should contain_glance_api_config('DEFAULT/rabbit_host').with_value('localhost2') } + it { should contain_glance_api_config('DEFAULT/rabbit_port').with_value('5673') } + it { should contain_glance_api_config('DEFAULT/rabbit_durable_queues').with_value('true') } + end + end + + describe 'with rabbit ssl cert parameters' do + let :params do + { + :rabbit_password => 'pass', + :rabbit_use_ssl => 'true', + :kombu_ssl_ca_certs => '/etc/ca.cert', + :kombu_ssl_certfile => '/etc/certfile', + :kombu_ssl_keyfile => '/etc/key', + :kombu_ssl_version => 'TLSv1', + } + end + + it { should contain_glance_api_config('DEFAULT/rabbit_use_ssl').with_value('true') } + it { should contain_glance_api_config('DEFAULT/kombu_ssl_ca_certs').with_value('/etc/ca.cert') } + it { should contain_glance_api_config('DEFAULT/kombu_ssl_certfile').with_value('/etc/certfile') } + it { should contain_glance_api_config('DEFAULT/kombu_ssl_keyfile').with_value('/etc/key') } + it { should contain_glance_api_config('DEFAULT/kombu_ssl_version').with_value('TLSv1') } + end + + describe 'with rabbit ssl disabled' do + let :params do + { + :rabbit_password => 'pass', + :rabbit_use_ssl => false, + :kombu_ssl_ca_certs => 'undef', + :kombu_ssl_certfile => 'undef', + :kombu_ssl_keyfile => 'undef', + :kombu_ssl_version => 'SSLv3', + } + end + + it { should contain_glance_api_config('DEFAULT/rabbit_use_ssl').with_value('false') } + it { should contain_glance_api_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') } + it { should contain_glance_api_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') } + it { should contain_glance_api_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') } + it { should contain_glance_api_config('DEFAULT/kombu_ssl_version').with_ensure('absent') } + end + + describe 'when passing params for single rabbit host' do + let :params do + { + :rabbit_password => 'pass', + :rabbit_userid => 'guest2', + :rabbit_host => 'localhost2', + :rabbit_port => '5673', + :rabbit_durable_queues => true, + } + end + it { should contain_glance_api_config('DEFAULT/rabbit_userid').with_value('guest2') } + it { should contain_glance_api_config('DEFAULT/rabbit_host').with_value('localhost2') } + it { should contain_glance_api_config('DEFAULT/rabbit_port').with_value('5673') } + it { should contain_glance_api_config('DEFAULT/rabbit_hosts').with_value('localhost2:5673') } + it { should contain_glance_api_config('DEFAULT/amqp_durable_queues').with_value('true') } + end + + describe 'when passing params for multiple rabbit hosts' do + let :params do + { + :rabbit_password => 'pass', + :rabbit_userid => 'guest3', + :rabbit_hosts => ['nonlocalhost3:5673', 'nonlocalhost4:5673'] + } + end + it { should contain_glance_api_config('DEFAULT/rabbit_userid').with_value('guest3') } + it { should contain_glance_api_config('DEFAULT/rabbit_hosts').with_value( + 'nonlocalhost3:5673,nonlocalhost4:5673') } + it { should contain_glance_api_config('DEFAULT/rabbit_ha_queues').with_value('true') } + it { should_not contain_glance_api_config('DEFAULT/rabbit_port') } + it { should_not contain_glance_api_config('DEFAULT/rabbit_host') } + end + + describe 'when using deprecated params' do + let :params do + { + :rabbit_durable_queues => true, + :rabbit_password => 'pass' + } + end + it { should contain_glance_api_config('DEFAULT/amqp_durable_queues').with_value('true') } + end +end diff --git a/glance/spec/classes/glance_registry_spec.rb b/glance/spec/classes/glance_registry_spec.rb new file mode 100644 index 000000000..28fd59af3 --- /dev/null +++ b/glance/spec/classes/glance_registry_spec.rb @@ -0,0 +1,338 @@ + +describe 'glance::registry' do + + let :facts do + { + :osfamily => 'Debian' + } + end + + let :default_params do + { + :verbose => false, + :debug => false, + :bind_host => '0.0.0.0', + :bind_port => '9191', + :log_file => '/var/log/glance/registry.log', + :log_dir => '/var/log/glance', + :database_connection => 'sqlite:///var/lib/glance/glance.sqlite', + :database_idle_timeout => '3600', + :enabled => true, + :manage_service => true, + :auth_type => 'keystone', + :auth_host => '127.0.0.1', + :auth_port => '35357', + :auth_protocol => 'http', + :auth_uri => 'http://127.0.0.1:5000/', + :keystone_tenant => 'services', + :keystone_user => 'glance', + :keystone_password => 'ChangeMe', + :purge_config => false, + } + end + + [ + {:keystone_password => 'ChangeMe'}, + { + :verbose => true, + :debug => true, + :bind_host => '127.0.0.1', + :bind_port => '9111', + :database_connection => 'sqlite:///var/lib/glance.sqlite', + :database_idle_timeout => '360', + :enabled => false, + :auth_type => 'keystone', + :auth_host => '127.0.0.1', + :auth_port => '35357', + :auth_protocol => 'http', + :auth_uri => 'http://127.0.0.1:5000/', + :keystone_tenant => 'admin', + :keystone_user => 'admin', + :keystone_password => 'ChangeMe', + } + ].each do |param_set| + + describe "when #{param_set == {:keystone_password => 'ChangeMe'} ? "using default" : "specifying"} class parameters" do + let :param_hash do + default_params.merge(param_set) + end + + let :params do + param_set + end + + it { should contain_class 'glance::registry' } + + it { should contain_service('glance-registry').with( + 'ensure' => (param_hash[:manage_service] && param_hash[:enabled]) ? 'running' : 'stopped', + 'enable' => param_hash[:enabled], + 'hasstatus' => true, + 'hasrestart' => true, + 'subscribe' => 'File[/etc/glance/glance-registry.conf]', + 'require' => 'Class[Glance]' + )} + + it 'should only sync the db if the service is enabled' do + + if param_hash[:enabled] + should contain_exec('glance-manage db_sync').with( + 'path' => '/usr/bin', + 'refreshonly' => true, + 'logoutput' => 'on_failure', + 'subscribe' => ['Package[glance-registry]', 'File[/etc/glance/glance-registry.conf]'], + 'notify' => 'Service[glance-registry]' + ) + end + end + it 'should configure itself' do + [ + 'verbose', + 'debug', + 'bind_port', + 'bind_host', + ].each do |config| + should contain_glance_registry_config("DEFAULT/#{config}").with_value(param_hash[config.intern]) + end + [ + 'database_connection', + 'database_idle_timeout', + ].each do |config| + should contain_glance_registry_config("database/#{config.gsub(/database_/,'')}").with_value(param_hash[config.intern]) + end + [ + 'auth_host', + 'auth_port', + 'auth_protocol' + ].each do |config| + should contain_glance_registry_config("keystone_authtoken/#{config}").with_value(param_hash[config.intern]) + end + should contain_glance_registry_config('keystone_authtoken/auth_admin_prefix').with_ensure('absent') + if param_hash[:auth_type] == 'keystone' + should contain_glance_registry_config("paste_deploy/flavor").with_value('keystone') + should contain_glance_registry_config("keystone_authtoken/admin_tenant_name").with_value(param_hash[:keystone_tenant]) + should contain_glance_registry_config("keystone_authtoken/admin_user").with_value(param_hash[:keystone_user]) + should contain_glance_registry_config("keystone_authtoken/admin_password").with_value(param_hash[:keystone_password]) + should contain_glance_registry_config("keystone_authtoken/admin_password").with_value(param_hash[:keystone_password]).with_secret(true) + end + end + end + end + + describe 'with disabled service managing' do + let :params do + { + :keystone_password => 'ChangeMe', + :manage_service => false, + :enabled => false, + } + end + + it { should contain_service('glance-registry').with( + 'ensure' => nil, + 'enable' => false, + 'hasstatus' => true, + 'hasrestart' => true, + 'subscribe' => 'File[/etc/glance/glance-registry.conf]', + 'require' => 'Class[Glance]' + )} + end + + describe 'with overridden pipeline' do + # At the time of writing there was only blank and keystone as options + # but there is no reason that there can't be more options in the future. + let :params do + { + :keystone_password => 'ChangeMe', + :pipeline => 'validoptionstring', + } + end + + it { should contain_glance_registry_config('paste_deploy/flavor').with_value('validoptionstring') } + end + + describe 'with blank pipeline' do + let :params do + { + :keystone_password => 'ChangeMe', + :pipeline => '', + } + end + + it { should contain_glance_registry_config('paste_deploy/flavor').with_ensure('absent') } + end + + [ + 'keystone/', + 'keystone+', + '+keystone', + 'keystone+cachemanagement+', + '+' + ].each do |pipeline| + describe "with pipeline incorrect value #{pipeline}" do + let :params do + { + :keystone_password => 'ChangeMe', + :auth_type => 'keystone', + :pipeline => pipeline + } + end + + it { expect { should contain_glance_registry_config('filter:paste_deploy/flavor') }.to\ + raise_error(Puppet::Error, /validate_re\(\): .* does not match/) } + end + end + + describe 'with overriden auth_admin_prefix' do + let :params do + { + :keystone_password => 'ChangeMe', + :auth_admin_prefix => '/keystone/main' + } + end + + it { should contain_glance_registry_config('keystone_authtoken/auth_admin_prefix').with_value('/keystone/main') } + end + + [ + '/keystone/', + 'keystone/', + 'keystone', + '/keystone/admin/', + 'keystone/admin/', + 'keystone/admin' + ].each do |auth_admin_prefix| + describe "with auth_admin_prefix_containing incorrect value #{auth_admin_prefix}" do + let :params do + { + :keystone_password => 'ChangeMe', + :auth_admin_prefix => auth_admin_prefix + } + end + + it { expect { should contain_glance_registry_config('filter:authtoken/auth_admin_prefix') }.to\ + raise_error(Puppet::Error, /validate_re\(\): "#{auth_admin_prefix}" does not match/) } + end + end + + describe 'with syslog disabled by default' do + let :params do + default_params + end + + it { should contain_glance_registry_config('DEFAULT/use_syslog').with_value(false) } + it { should_not contain_glance_registry_config('DEFAULT/syslog_log_facility') } + end + + describe 'with syslog enabled' do + let :params do + default_params.merge({ + :use_syslog => 'true', + }) + end + + it { should contain_glance_registry_config('DEFAULT/use_syslog').with_value(true) } + it { should contain_glance_registry_config('DEFAULT/syslog_log_facility').with_value('LOG_USER') } + end + + describe 'with syslog enabled and custom settings' do + let :params do + default_params.merge({ + :use_syslog => 'true', + :log_facility => 'LOG_LOCAL0' + }) + end + + it { should contain_glance_registry_config('DEFAULT/use_syslog').with_value(true) } + it { should contain_glance_registry_config('DEFAULT/syslog_log_facility').with_value('LOG_LOCAL0') } + end + + describe 'with log_file enabled by default' do + let(:params) { default_params } + + it { should contain_glance_registry_config('DEFAULT/log_file').with_value(default_params[:log_file]) } + + context 'with log_file disabled' do + let(:params) { default_params.merge!({ :log_file => false }) } + it { should contain_glance_registry_config('DEFAULT/log_file').with_ensure('absent') } + end + end + + describe 'with log_dir enabled by default' do + let(:params) { default_params } + + it { should contain_glance_registry_config('DEFAULT/log_dir').with_value(default_params[:log_dir]) } + + context 'with log_dir disabled' do + let(:params) { default_params.merge!({ :log_dir => false }) } + it { should contain_glance_registry_config('DEFAULT/log_dir').with_ensure('absent') } + end + end + + describe 'with no ssl options (default)' do + let(:params) { default_params } + + it { should contain_glance_registry_config('DEFAULT/ca_file').with_ensure('absent')} + it { should contain_glance_registry_config('DEFAULT/cert_file').with_ensure('absent')} + it { should contain_glance_registry_config('DEFAULT/key_file').with_ensure('absent')} + end + + describe 'with ssl options' do + let :params do + default_params.merge({ + :ca_file => '/tmp/ca_file', + :cert_file => '/tmp/cert_file', + :key_file => '/tmp/key_file' + }) + end + + context 'with ssl options' do + it { should contain_glance_registry_config('DEFAULT/ca_file').with_value('/tmp/ca_file') } + it { should contain_glance_registry_config('DEFAULT/cert_file').with_value('/tmp/cert_file') } + it { should contain_glance_registry_config('DEFAULT/key_file').with_value('/tmp/key_file') } + end + end + + describe 'with deprecated sql parameters' do + let :params do + default_params.merge({ + :sql_connection => 'mysql://user:pass@db/db', + :sql_idle_timeout => '30' + }) + end + + it 'configures database' do + should contain_glance_registry_config('database/connection').with_value('mysql://user:pass@db/db') + should contain_glance_registry_config('database/idle_timeout').with_value('30') + end + end + + describe 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + let(:params) { default_params } + + it {should contain_package('glance-registry')} + end + + describe 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + let(:params) { default_params } + + it { should contain_package('openstack-glance')} + end + + describe 'on unknown platforms' do + let :facts do + { :osfamily => 'unknown' } + end + let(:params) { default_params } + + it 'should fails to configure glance-registry' do + expect { subject }.to raise_error(Puppet::Error, /module glance only support osfamily RedHat and Debian/) + end + end + +end diff --git a/glance/spec/classes/glance_spec.rb b/glance/spec/classes/glance_spec.rb new file mode 100644 index 000000000..9af348a2f --- /dev/null +++ b/glance/spec/classes/glance_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe 'glance' do + + let :facts do + { + :osfamily => 'Debian' + } + end + + let :default_params do + {} + end + + [ + {}, + {} + ].each do |param_set| + + describe "when #{param_set == {} ? "using default" : "specifying"} class parameters" do + + let :param_hash do + param_set == {} ? default_params : params + end + + let :params do param_set end + + it { should contain_file('/etc/glance/').with( + 'ensure' => 'directory', + 'owner' => 'glance', + 'mode' => '0770' + )} + + end + end + + describe 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + let(:params) { default_params } + + it {should_not contain_package('glance')} + end + + describe 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + let(:params) { default_params } + + it { should contain_package('openstack-glance')} + end + +end diff --git a/glance/spec/shared_examples.rb b/glance/spec/shared_examples.rb new file mode 100644 index 000000000..d92156a36 --- /dev/null +++ b/glance/spec/shared_examples.rb @@ -0,0 +1,5 @@ +shared_examples_for "a Puppet::Error" do |description| + it "with message matching #{description.inspect}" do + expect { should have_class_count(1) }.to raise_error(Puppet::Error, description) + end +end diff --git a/glance/spec/spec.opts b/glance/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/glance/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/glance/spec/spec_helper.rb b/glance/spec/spec_helper.rb new file mode 100644 index 000000000..076e2bb39 --- /dev/null +++ b/glance/spec/spec_helper.rb @@ -0,0 +1,7 @@ +require 'puppetlabs_spec_helper/module_spec_helper' +require 'shared_examples' + +RSpec.configure do |c| + c.alias_it_should_behave_like_to :it_configures, 'configures' + c.alias_it_should_behave_like_to :it_raises, 'raises' +end diff --git a/glance/spec/unit/provider/glance_spec.rb b/glance/spec/unit/provider/glance_spec.rb new file mode 100644 index 000000000..315b0b319 --- /dev/null +++ b/glance/spec/unit/provider/glance_spec.rb @@ -0,0 +1,59 @@ +require 'puppet' +require 'spec_helper' +require 'puppet/provider/glance' +require 'tempfile' + + +klass = Puppet::Provider::Glance + +describe Puppet::Provider::Glance do + + after :each do + klass.reset + end + + describe 'when retrieving the auth credentials' do + + it 'should fail if the glance config file does not have the expected contents' do + mock = {} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/glance/glance-api.conf') + expect do + klass.glance_credentials + end.to raise_error(Puppet::Error, /does not contain all required sections/) + end + + describe 'when testing glance connection retries' do + + ['[Errno 111] Connection refused', '(HTTP 400)', 'HTTP Unable to establish connection'].reverse.each do |valid_message| + it "should retry when glance is not ready with error #{valid_message}" do + mock = {'keystone_authtoken' => + { + 'auth_host' => '127.0.0.1', + 'auth_port' => '35357', + 'auth_protocol' => 'http', + 'admin_tenant_name' => 'foo', + 'admin_user' => 'user', + 'admin_password' => 'pass' + } + } + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/glance/glance-api.conf') + klass.expects(:sleep).with(10).returns(nil) + klass.expects(:glance).twice.with( + '--os-tenant-name', + 'foo', + '--os-username', + 'user', + '--os-password', + 'pass', + '--os-auth-url', + 'http://127.0.0.1:35357/v2.0/', + ['test_retries'] + ).raises(Exception, valid_message).then.returns('') + klass.auth_glance('test_retries') + end + end + end + end +end diff --git a/glance/tests/api.pp b/glance/tests/api.pp new file mode 100644 index 000000000..7b4931916 --- /dev/null +++ b/glance/tests/api.pp @@ -0,0 +1,4 @@ +class { 'glance::api': + debug => true, + verbose => true, +} diff --git a/glance/tests/init.pp b/glance/tests/init.pp new file mode 100644 index 000000000..57cf401a1 --- /dev/null +++ b/glance/tests/init.pp @@ -0,0 +1 @@ +class { 'glance': } diff --git a/glance/tests/registry.pp b/glance/tests/registry.pp new file mode 100644 index 000000000..8635cb267 --- /dev/null +++ b/glance/tests/registry.pp @@ -0,0 +1,4 @@ +class { 'glance::registry': + debug => true, + verbose => true, +} diff --git a/glance/tests/site.pp b/glance/tests/site.pp new file mode 100644 index 000000000..2859b72d7 --- /dev/null +++ b/glance/tests/site.pp @@ -0,0 +1,57 @@ + +# uses the keystone packages +# to ensure that we use the latest precise packages +Exec { logoutput => 'on_failure' } + +node glance_keystone_mysql { + class { 'mysql::server': } + class { 'keystone': + verbose => true, + debug => true, + catalog_type => 'sql', + admin_token => 'admin_token', + } + class { 'keystone::db::mysql': + password => 'keystone', + } + class { 'keystone::roles::admin': + email => 'test@puppetlabs.com', + password => 'ChangeMe', + } + class { 'glance::api': + verbose => true, + debug => true, + auth_type => 'keystone', + keystone_tenant => 'services', + keystone_user => 'glance', + keystone_password => 'glance_password', + sql_connection => 'mysql://glance:glance@127.0.0.1/glance', + } + class { 'glance::backend::file': } + + class { 'glance::db::mysql': + password => 'glance', + dbname => 'glance', + user => 'glance', + host => '127.0.0.1', + # allowed_hosts = undef, + # $cluster_id = 'localzone' + } + + class { 'glance::registry': + verbose => true, + debug => true, + auth_type => 'keystone', + keystone_tenant => 'services', + keystone_user => 'glance', + keystone_password => 'glance_password', + sql_connection => 'mysql://glance:glance@127.0.0.1/glance', + } + class { 'glance::keystone::auth': + password => 'glance_pass', + } +} + +node default { + fail("could not find a matching node entry for ${clientcert}") +} diff --git a/gluster b/gluster deleted file mode 160000 index 6c962083d..000000000 --- a/gluster +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6c962083d8b100dcaeb6f11dbe61e6071f3d13f0 diff --git a/gluster/.gitignore b/gluster/.gitignore new file mode 100644 index 000000000..729179bea --- /dev/null +++ b/gluster/.gitignore @@ -0,0 +1,7 @@ +old/ +tmp/ +pkg/ +docs/ +hacking/ +rpmbuild/ +screencasts/ diff --git a/gluster/.gitmodules b/gluster/.gitmodules new file mode 100644 index 000000000..bc295c271 --- /dev/null +++ b/gluster/.gitmodules @@ -0,0 +1,24 @@ +[submodule "vagrant/puppet/modules/stdlib"] + path = vagrant/puppet/modules/stdlib + url = https://github.com/purpleidea/puppetlabs-stdlib.git +[submodule "vagrant/puppet/modules/apt"] + path = vagrant/puppet/modules/apt + url = https://github.com/purpleidea/puppetlabs-apt.git +[submodule "vagrant/puppet/modules/common"] + path = vagrant/puppet/modules/common + url = https://github.com/purpleidea/puppet-common.git +[submodule "vagrant/puppet/modules/keepalived"] + path = vagrant/puppet/modules/keepalived + url = https://github.com/purpleidea/puppet-keepalived.git +[submodule "vagrant/puppet/modules/puppet"] + path = vagrant/puppet/modules/puppet + url = https://github.com/purpleidea/puppet-puppet.git +[submodule "vagrant/puppet/modules/shorewall"] + path = vagrant/puppet/modules/shorewall + url = https://github.com/purpleidea/puppet-shorewall.git +[submodule "vagrant/puppet/modules/yum"] + path = vagrant/puppet/modules/yum + url = https://github.com/purpleidea/puppet-yum.git +[submodule "vagrant/puppet/modules/module-data"] + path = vagrant/puppet/modules/module-data + url = https://github.com/purpleidea/puppet-module-data.git diff --git a/gluster/COPYING b/gluster/COPYING new file mode 100644 index 000000000..dba13ed2d --- /dev/null +++ b/gluster/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/gluster/COPYRIGHT b/gluster/COPYRIGHT new file mode 100644 index 000000000..480149fb7 --- /dev/null +++ b/gluster/COPYRIGHT @@ -0,0 +1,16 @@ +Copyright (C) 2012-2013+ James Shubin +Written by James Shubin + +This puppet module is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This puppet module 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + diff --git a/gluster/DOCUMENTATION.md b/gluster/DOCUMENTATION.md new file mode 100644 index 000000000..e1cc82c3c --- /dev/null +++ b/gluster/DOCUMENTATION.md @@ -0,0 +1,964 @@ +#Puppet-Gluster + +##A GlusterFS Puppet module by [James](https://ttboj.wordpress.com/) +####Available from: +####[https://github.com/purpleidea/puppet-gluster/](https://github.com/purpleidea/puppet-gluster/) + +####Also available from: +####[https://forge.gluster.org/puppet-gluster/](https://forge.gluster.org/puppet-gluster/) + +####This documentation is available in: [Markdown](https://github.com/purpleidea/puppet-gluster/blob/master/DOCUMENTATION.md) or [PDF](https://github.com/purpleidea/puppet-gluster/raw/master/puppet-gluster-documentation.pdf) format. + +####Table of Contents + +1. [Overview](#overview) +2. [Module description - What the module does](#module-description) +3. [Setup - Getting started with Puppet-Gluster](#setup) + * [What can Puppet-Gluster manage?](#what-can-puppet-gluster-manage) + * [Simple setup](#simple-setup) + * [Elastic setup](#elastic-setup) + * [Advanced setup](#advanced-setup) + * [Client setup](#client-setup) +4. [Usage/FAQ - Notes on management and frequently asked questions](#usage-and-frequently-asked-questions) +5. [Reference - Class and type reference](#reference) + * [gluster::simple](#glustersimple) + * [gluster::elastic](#glusterelastic) + * [gluster::server](#glusterserver) + * [gluster::host](#glusterhost) + * [gluster::brick](#glusterbrick) + * [gluster::volume](#glustervolume) + * [gluster::volume::property](#glustervolumeproperty) + * [gluster::mount](#glustermount) +6. [Examples - Example configurations](#examples) +7. [Limitations - Puppet versions, OS compatibility, etc...](#limitations) +8. [Development - Background on module development and reporting bugs](#development) +9. [Author - Author and contact information](#author) + +##Overview + +The Puppet-Gluster module installs, configures, and manages a GlusterFS cluster. + +##Module Description + +This Puppet-Gluster module handles installation, configuration, and management +of GlusterFS across all of the hosts in the cluster. + +##Setup + +###What can Puppet-Gluster manage? + +Puppet-Gluster is designed to be able to manage as much or as little of your +GlusterFS cluster as you wish. All features are optional. If there is a feature +that doesn't appear to be optional, and you believe it should be, please let me +know. Having said that, it makes good sense to me to have Puppet-Gluster manage +as much of your GlusterFS infrastructure as it can. At the moment, it cannot +rack new servers, but I am accepting funding to explore this feature ;) At the +moment it can manage: + +* GlusterFS packages (rpm) +* GlusterFS configuration files (/var/lib/glusterd/) +* GlusterFS host peering (gluster peer probe) +* GlusterFS storage partitioning (fdisk) +* GlusterFS storage formatting (mkfs) +* GlusterFS brick creation (mkdir) +* GlusterFS services (glusterd) +* GlusterFS firewalling (whitelisting) +* GlusterFS volume creation (gluster volume create) +* GlusterFS volume state (started/stopped) +* GlusterFS volume properties (gluster volume set) +* And much more... + +###Simple setup + +include '::gluster::simple' is enough to get you up and running. When using the +gluster::simple class, or with any other Puppet-Gluster configuration, +identical definitions must be used on all hosts in the cluster. The simplest +way to accomplish this is with a single shared puppet host definition like: + +```puppet +node /^annex\d+$/ { # annex{1,2,..N} + class { '::gluster::simple': + } +} +``` + +If you wish to pass in different parameters, you can specify them in the class +before you provision your hosts: + +```puppet +class { '::gluster::simple': + replica => 2, + volume => ['volume1', 'volume2', 'volumeN'], +} +``` + +###Elastic setup + +The gluster::elastic class is not yet available. Stay tuned! + +###Advanced setup + +Some system administrators may wish to manually itemize each of the required +components for the Puppet-Gluster deployment. This happens automatically with +the higher level modules, but may still be a desirable feature, particularly +for non-elastic storage pools where the configuration isn't expected to change +very often (if ever). + +To put together your cluster piece by piece, you must manually include and +define each class and type that you wish to use. If there are certain aspects +that you wish to manage yourself, you can omit them from your configuration. +See the [reference](#reference) section below for the specifics. Here is one +possible example: + +```puppet +class { '::gluster::server': + shorewall => true, +} + +gluster::host { 'annex1.example.com': + # use uuidgen to make these + uuid => '1f660ca2-2c78-4aa0-8f4d-21608218c69c', +} + +# note that this is using a folder on your existing file system... +# this can be useful for prototyping gluster using virtual machines +# if this isn't a separate partition, remember that your root fs will +# run out of space when your gluster volume does! +gluster::brick { 'annex1.example.com:/data/gluster-storage1': + areyousure => true, +} + +gluster::host { 'annex2.example.com': + # NOTE: specifying a host uuid is now optional! + # if you don't choose one, one will be assigned + #uuid => '2fbe6e2f-f6bc-4c2d-a301-62fa90c459f8', +} + +gluster::brick { 'annex2.example.com:/data/gluster-storage2': + areyousure => true, +} + +$brick_list = [ + 'annex1.example.com:/data/gluster-storage1', + 'annex2.example.com:/data/gluster-storage2', +] + +gluster::volume { 'examplevol': + replica => 2, + bricks => $brick_list, + start => undef, # i'll start this myself +} + +# namevar must be: # +gluster::volume::property { 'examplevol#auth.reject': + value => ['192.0.2.13', '198.51.100.42', '203.0.113.69'], +} +``` + +###Client setup + +Mounting a GlusterFS volume on a client is fairly straightforward. Simply use +the 'gluster::mount' type. + +```puppet + gluster::mount { '/mnt/gluster/puppet/': + server => 'annex.example.com:/puppet', + rw => true, + shorewall => false, + } +``` + +In this example, 'annex.example.com' points to the VIP of the GlusterFS +cluster. Using the VIP for mounting increases the chance that you'll get an +available server when you try to mount. This generally works better than RRDNS +or similar schemes. + +##Usage and frequently asked questions + +All management should be done by manipulating the arguments on the appropriate +Puppet-Gluster classes and types. Since certain manipulations are either not +yet possible with Puppet-Gluster, or are not supported by GlusterFS, attempting +to manipulate the Puppet configuration in an unsupported way will result in +undefined behaviour, and possible even data loss, however this is unlikely. + +###How do I change the replica count? + +You must set this before volume creation. This is a limitation of GlusterFS. +There are certain situations where you can change the replica count by adding +a multiple of the existing brick count to get this desired effect. These cases +are not yet supported by Puppet-Gluster. If you want to use Puppet-Gluster +before and / or after this transition, you can do so, but you'll have to do the +changes manually. + +###Do I need to use a virtual IP? + +Using a virtual IP (VIP) is strongly recommended as a distributed lock manager +(DLM) and also to provide a highly-available (HA) IP address for your clients +to connect to. For a more detailed explanation of the reasoning please see: + +[How to avoid cluster race conditions or: How to implement a distributed lock manager in puppet](https://ttboj.wordpress.com/2012/08/23/how-to-avoid-cluster-race-conditions-or-how-to-implement-a-distributed-lock-manager-in-puppet/) + +Remember that even if you're using a hosted solution (such as AWS) that doesn't +provide an additional IP address, or you want to avoid using an additional IP, +and you're okay not having full HA client mounting, you can use an unused +private RFC1918 IP address as the DLM VIP. Remember that a layer 3 IP can +co-exist on the same layer 2 network with the layer 3 network that is used by +your cluster. + +###Is it possible to have Puppet-Gluster complete in a single run? + +No. This is a limitation of Puppet, and is related to how GlusterFS operates. +For example, it is not reliably possible to predict which ports a particular +GlusterFS volume will run on until after the volume is started. As a result, +this module will initially whitelist connections from GlusterFS host IP +addresses, and then further restrict this to only allow individual ports once +this information is known. This is possible in conjunction with the +[puppet-shorewall](https://github.com/purpleidea/puppet-shorewall) module. +You should notice that each run should complete without error. If you do see an +error, it means that either something is wrong with your system and / or +configuration, or because there is a bug in Puppet-Gluster. + +###Can you integrate this with vagrant? + +Yes, see the +[vagrant/](https://github.com/purpleidea/puppet-gluster/tree/master/vagrant) +directory. This has been tested on Fedora 20, with vagrant-libvirt, as I have +no desire to use VirtualBox for fun. I have written an article about this: + +[Automatically deploying GlusterFS with Puppet-Gluster + Vagrant!](https://ttboj.wordpress.com/2014/01/08/automatically-deploying-glusterfs-with-puppet-gluster-vagrant/) + +You'll probably first need to read my three earlier articles to learn some +vagrant tricks, and to get the needed dependencies installed: + +* [Vagrant on Fedora with libvirt](https://ttboj.wordpress.com/2013/12/09/vagrant-on-fedora-with-libvirt/) +* [Vagrant vsftp and other tricks](https://ttboj.wordpress.com/2013/12/21/vagrant-vsftp-and-other-tricks/) +* [Vagrant clustered SSH and ‘screen’](https://ttboj.wordpress.com/2014/01/02/vagrant-clustered-ssh-and-screen/) + +###Puppet runs fail with "Invalid relationship" errors. + +When running Puppet, you encounter a compilation failure like: + +```bash +Error: Could not retrieve catalog from remote server: +Error 400 on SERVER: Invalid relationship: Exec[gluster-volume-stuck-volname] { +require => Gluster::Brick[annex2.example.com:/var/lib/puppet/tmp/gluster/data/] +}, because Gluster::Brick[annex2.example.com:/var/lib/puppet/tmp/gluster/data/] +doesn't seem to be in the catalog +Warning: Not using cache on failed catalog +Error: Could not retrieve catalog; skipping run +``` + +This can occur if you have changed (usually removed) the available bricks, but +have not cleared the exported resources on the Puppet master, or if there are +stale (incorrect) brick "tags" on the individual host. These tags can usually +be found in the _/var/lib/puppet/tmp/gluster/brick/_ directory. In other words, +when a multi host cluster comes up, each puppet agent tells the master about +which bricks it has available, and each agent also pulls down this list and +stores it in the brick directory. If there is a discrepancy, then the compile +will fail because the individual host is using old data as part of its facts +when it uses the stale brick data as part of its compilation. + +This commonly happens if you're trying to deploy a different Puppet-Gluster +setup without having first erased the host specific exported resources on the +Puppet master or if the machine hasn't been re-provisioned from scratch. + +To solve this problem, do a clean install, and make sure that you've cleaned +the Puppet master with: + +```bash +puppet node deactivate HOSTNAME +``` + +for each host you're using, and that you've removed all of the files from the +brick directories on each host. + +###Puppet runs fail with "Connection refused - connect(2)" errors. + +You may see a "_Connection refused - connect(2)_" message when running puppet. +This typically happens if your puppet vm guest is overloaded. When running high +guest counts on your laptop, or running without hardware virtualization support +this is quite common. Another common causes of this is if your domain type is +set to _qemu_ instead of the accelerated _kvm_. Since the _qemu_ domain type is +much slower, puppet timeouts and failures are common when it doesn't respond. + +###Provisioning fails with: "Can't open /dev/sdb1 exclusively." + +If when provisioning you get an error like: + +_"Can't open /dev/sdb1 exclusively. Mounted filesystem?"_ + +It is possible that dracut might have found an existing logical volume on the +device, and device mapper has made it available. This is common if you are +re-using dirty block devices that haven't run through a _dd_ first. Here is an +example of the diagnosis and treatment of this problem: + +```bash +[root@server mapper]# pwd +/dev/mapper +[root@server mapper]# dmesg | grep dracut +dracut: dracut-004-336.el6_5.2 +dracut: rd_NO_LUKS: removing cryptoluks activation +dracut: Starting plymouth daemon +dracut: rd_NO_DM: removing DM RAID activation +dracut: rd_NO_MD: removing MD RAID activation +dracut: Scanning devices sda3 sdb for LVM logical volumes myvg/rootvol +dracut: inactive '/dev/vg_foo/lv' [4.35 TiB] inherit +dracut: inactive '/dev/myvg/rootvol' [464.00 GiB] inherit +dracut: Mounted root filesystem /dev/mapper/myvg-rootvol +dracut: Loading SELinux policy +dracut: +dracut: Switching root +[root@server mapper]# /sbin/pvcreate --dataalignment 2560K /dev/sdb1 + Can't open /dev/sdb1 exclusively. Mounted filesystem? +[root@server mapper]# ls +control myvg-rootvol vg_foo-lv +[root@server mapper]# ls -lAh +total 0 +crw-rw----. 1 root root 10, 58 Mar 7 16:42 control +lrwxrwxrwx. 1 root root 7 Mar 13 09:56 myvg-rootvol -> ../dm-0 +lrwxrwxrwx. 1 root root 7 Mar 13 09:56 vg_foo-lv -> ../dm-1 +[root@server mapper]# dmsetup remove vg_foo-lv +[root@server mapper]# ls +control myvg-rootvol +[root@server mapper]# pvcreate --dataalignment 2560K /dev/sdb1 + Physical volume "/dev/sdb1" successfully created +[root@server mapper]# HAPPY_ADMIN='yes' +``` + +If you frequently start with "dirty" block devices, you may consider adding a +_dd_ to your hardware provisioning step. The downside is that this can be very +time consuming, and potentially dangerous if you accidentally re-provision the +wrong machine. + +###Provisioning fails with: "cannot open /dev/sdb1: Device or resource busy" + +If when provisioning you get an error like: + +_"mkfs.xfs: cannot open /dev/sdb1: Device or resource busy"_ + +It is possible that dracut might have found an existing logical volume on the +device, and device mapper has made it available. This is common if you are +re-using dirty block devices that haven't run through a _dd_ first. This is +almost identical to the previous frequently asked question, although this +failure message is what is seen when _mkfs.xfs_ is being blocked by dracut, +where in the former problem it was the _pvcreate_ that was being blocked. The +reason that we see this manifest through _mkfs.xfs_ instead of _pvcreate_ is +that this particular cluster is being build with _lvm => false_. Here is an +example of the diagnosis and treatment of this problem: + +```bash +[root@server mapper]# pwd +/dev/mapper +[root@server mapper]# dmesg | grep dracut +dracut: dracut-004-335.el6 +dracut: rd_NO_LUKS: removing cryptoluks activation +dracut: Starting plymouth daemon +dracut: rd_NO_DM: removing DM RAID activation +dracut: rd_NO_MD: removing MD RAID activation +dracut: Scanning devices sda2 sdb for LVM logical volumes vg_server/lv_swap vg_server/lv_root +dracut: inactive '/dev/vg_bricks/b1' [9.00 TiB] inherit +dracut: inactive '/dev/vg_server/lv_root' [50.00 GiB] inherit +dracut: inactive '/dev/vg_server/lv_home' [383.26 GiB] inherit +dracut: inactive '/dev/vg_server/lv_swap' [31.50 GiB] inherit +dracut: Mounted root filesystem /dev/mapper/vg_server-lv_root +dracut: +dracut: Switching root +[root@server mapper]# mkfs.xfs -q -f -i size=512 -n size=8192 /dev/sdb1 +mkfs.xfs: cannot open /dev/sdb1: Device or resource busy +[root@server mapper]# lsof /dev/sdb1 +[root@server mapper]# echo $? +1 +[root@server mapper]# ls +control vg_server-lv_home vg_server-lv_swap +vg_bricks-b1 vg_server-lv_root +[root@server mapper]# ls -lAh +total 0 +crw-rw---- 1 root root 10, 58 May 20 2014 control +lrwxrwxrwx 1 root root 7 May 20 2014 vg_bricks-b1 -> ../dm-2 +lrwxrwxrwx 1 root root 7 May 20 2014 vg_server-lv_home -> ../dm-3 +lrwxrwxrwx 1 root root 7 May 20 2014 vg_server-lv_root -> ../dm-0 +lrwxrwxrwx 1 root root 7 May 20 2014 vg_server-lv_swap -> ../dm-1 +[root@server mapper]# dmsetup remove vg_bricks-b1 +[root@server mapper]# ls +control vg_server-lv_home vg_server-lv_root vg_server-lv_swap +[root@server mapper]# mkfs.xfs -q -f -i size=512 -n size=8192 /dev/sdb1 +[root@server mapper]# echo $? +0 +[root@server mapper]# HAPPY_ADMIN='yes' +``` + +If you frequently start with "dirty" block devices, you may consider adding a +_dd_ to your hardware provisioning step. The downside is that this can be very +time consuming, and potentially dangerous if you accidentally re-provision the +wrong machine. + +###I changed the hardware manually, and now my system won't boot. + +If you're using Puppet-Gluster to manage storage, the filesystem will be +mounted with _UUID_ entries in _/etc/fstab_. This ensures that the correct +filesystem will be mounted, even if the device order changes. If a filesystem +is not available at boot time, startup will abort and offer you the chance to +go into read-only maintenance mode. Either fix the hardware issue, or edit the +_/etc/fstab_ file. + + +###I can't edit /etc/fstab in the maintenance shell because it is read-only. + +In the maintenance shell, your root filesystem will be mounted read-only, to +prevent changes. If you need to edit a file such as _/etc/fstab_, you'll first +need to remount the root filesystem in read-write mode. You can do this with: + +```bash +mount -n -o remount / +``` + +###I get a file dependency error when running Puppet-Gluster. + +In order for Puppet-Gluster to be able to do its magic, it needs to store some +temporary files on each GlusterFS host. These files usually get stored in: +_/var/lib/puppet/tmp/gluster/_. The parent directory (_/var/lib/puppet/tmp/_) +gets created by the _puppet::vardir_ module. The error you'll typically see is: + +```bash +Error: Failed to apply catalog: Could not find dependency +File[/var/lib/puppet/tmp/] for File[/var/lib/puppet/tmp/gluster/] at +/etc/puppet/modules/gluster/manifests/vardir.pp:49 +``` + +This error occurs if you forget to _include_ the _puppet::vardir_ class from +the [puppet-puppet](https://github.com/purpleidea/puppet-puppet/) module. If +you don't want to include the entire module, you can pull in the +_puppet::vardir_ class by itself, or create the contained file type manually in +your puppet manifests. + +###Will this work on my favourite OS? (eg: GNU/Linux F00bar OS v12 ?) +If it's a GNU/Linux based OS, can run GlusterFS, and Puppet, then it will +probably work. Typically, you might need to add a yaml data file to the _data/_ +folder so that Puppet-Gluster knows where certain operating system specific +things are found. The multi-distro support has been designed to make it +particularly easy to add support for additional platforms. If your platform +doesn't work, please submit a yaml data file with the platform specific values. + +###Awesome work, but it's missing support for a feature and/or platform! + +Since this is an Open Source / Free Software project that I also give away for +free (as in beer, free as in gratis, free as in libre), I'm unable to provide +unlimited support. Please consider donating funds, hardware, virtual machines, +and other resources. For specific needs, you could perhaps sponsor a feature! + +###You didn't answer my question, or I have a question! + +Contact me through my [technical blog](https://ttboj.wordpress.com/contact/) +and I'll do my best to help. If you have a good question, please remind me to +add my answer to this documentation! + +##Reference +Please note that there are a number of undocumented options. For more +information on these options, please view the source at: +[https://github.com/purpleidea/puppet-gluster/](https://github.com/purpleidea/puppet-gluster/). +If you feel that a well used option needs documenting here, please contact me. + +###Overview of classes and types + +* [gluster::simple](#glustersimple): Simple Puppet-Gluster deployment. +* [gluster::elastic](#glusterelastic): Under construction. +* [gluster::server](#glusterserver): Base class for server hosts. +* [gluster::host](#glusterhost): Host type for each participating host. +* [gluster::brick](#glusterbrick): Brick type for each defined brick, per host. +* [gluster::volume](#glustervolume): Volume type for each defined volume. +* [gluster::volume::property](#glustervolumeproperty): Manages properties for each volume. +* [gluster::mount](#glustermount): Client volume mount point management. + +###gluster::simple +This is gluster::simple. It should probably take care of 80% of all use cases. +It is particularly useful for deploying quick test clusters. It uses a +finite-state machine (FSM) to decide when the cluster has settled and volume +creation can begin. For more information on the FSM in Puppet-Gluster see: +[https://ttboj.wordpress.com/2013/09/28/finite-state-machines-in-puppet/](https://ttboj.wordpress.com/2013/09/28/finite-state-machines-in-puppet/) + +####`replica` +The replica count. Can't be changed automatically after initial deployment. + +####`volume` +The volume name or list of volume names to create. + +####`path` +The valid brick path for each host. Defaults to local file system. If you need +a different path per host, then Gluster::Simple will not meet your needs. + +####`count` +Number of bricks to build per host. This value is used unless _brick_params_ is +being used. + +####`vip` +The virtual IP address to be used for the cluster distributed lock manager. +This option can be used in conjunction with the _vrrp_ option, but it does not +require it. If you don't want to provide a virtual ip, but you do want to +enforce that certain operations only run on one host, then you can set this +option to be the ip address of an arbitrary host in your cluster. Keep in mind +that if that host is down, certain options won't ever occur. + +####`vrrp` +Whether to automatically deploy and manage _Keepalived_ for use as a _DLM_ and +for use in volume mounting, etc... Using this option requires the _vip_ option. + +####`layout` +Which brick layout to use. The available options are: _chained_, and (default). +To generate a default (symmetrical, balanced) layout, leave this option blank. +If you'd like to include an algorithm that generates a different type of brick +layout, it is easy to drop in an algorithm. Please contact me with the details! + +####`version` +Which version of GlusterFS do you want to install? This is especially handy +when testing new beta releases. You can read more about the technique at: +[Testing GlusterFS during Glusterfest](https://ttboj.wordpress.com/2014/01/16/testing-glusterfs-during-glusterfest/). + +####`repo` +Whether or not to add the necessary software repositories to install the needed +packages. This will typically pull in GlusterFS from _download.gluster.org_ and +should be set to false if you have your own mirrors or repositories managed as +part of your base image. + +####`brick_params` +This parameter lets you specify a hash to use when creating the individual +bricks. This is especially useful because it lets you have the power of +Gluster::Simple when managing a cluster of iron (physical machines) where you'd +like to specify brick specific parameters. This sets the brick count when the +_count_ parameter is 0. The format of this parameter might look like: + +```bash +$brick_params = { + fqdn1 => [ + {dev => '/dev/disk/by-uuid/01234567-89ab-cdef-0123-456789abcdef'}, + {dev => '/dev/sdc', partition => false}, + ], + fqdn2 => [{ + dev => '/dev/disk/by-path/pci-0000:02:00.0-scsi-0:1:0:0', + raid_su => 256, raid_sw => 10, + }], + fqdnN => [...], +} +``` + +####`brick_param_defaults` +This parameter lets you specify a hash of defaults to use when creating each +brick with the _brick_params_ parameter. It is useful because it avoids the +need to repeat the values that are common across all bricks in your cluster. +Since most options work this way, this is an especially nice feature to have. +The format of this parameter might look like: + +```bash +$brick_param_defaults = { + lvm => false, + xfs_inode64 => true, + force => true, +} +``` + +####`brick_params_defaults` +This parameter lets you specify a list of defaults to use when creating each +brick. Each element in the list represents a different brick. The value of each +element is a hash with the actual defaults that you'd like to use for creating +that brick. If you do not specify a brick count by any other method, then the +number of elements in this array will be used as the brick count. This is very +useful if you have consistent device naming across your entire cluster, because +you can very easily specify the devices and brick counts once for all hosts. If +for some reason a particular device requires unique values, then it can be set +manually with the _brick_params_ parameter. Please note the spelling of this +parameter. It is not the same as the _brick_param_defaults_ parameter which is +a global defaults parameter which will apply to all bricks. +The format of this parameter might look like: + +```bash +$brick_params_defaults = [ + {'dev' => '/dev/sdb'}, + {'dev' => '/dev/sdc'}, + {'dev' => '/dev/sdd'}, + {'dev' => '/dev/sde'}, +] +``` + +####`setgroup` +Set a volume property group. The two most common or well-known groups are the +_virt_ group, and the _small-file-perf_ group. This functionality is emulated +whether you're using the RHS version of GlusterFS or if you're using the +upstream GlusterFS project, which doesn't (currently) have the _volume set +group_ command. As package managers update the list of available groups or +their properties, Puppet-Gluster will automatically keep your set group +up-to-date. It is easy to extend Puppet-Gluster to add a custom group without +needing to patch the GlusterFS source. + +####`ping` +Whether to use _fping_ or not to help with ensuring the required hosts are +available before doing certain types of operations. Optional, but recommended. +Boolean value. + +####`again` +Do you want to use _Exec['again']_ ? This helps build your cluster quickly! + +####`baseport` +Specify the base port option as used in the glusterd.vol file. This is useful +if the default port range of GlusterFS conflicts with the ports used for +virtual machine migration, or if you simply like to choose the ports that +you're using. Integer value. + +####`rpcauthallowinsecure` +This is needed in some setups in the glusterd.vol file, particularly (I think) +for some users of _libgfapi_. Boolean value. + +####`shorewall` +Boolean to specify whether puppet-shorewall integration should be used or not. + +###gluster::elastic +Under construction. + +###gluster::server +Main server class for the cluster. Must be included when building the GlusterFS +cluster manually. Wrapper classes such as [gluster::simple](#glustersimple) +include this automatically. + +####`vip` +The virtual IP address to be used for the cluster distributed lock manager. + +####`shorewall` +Boolean to specify whether puppet-shorewall integration should be used or not. + +###gluster::host +Main host type for the cluster. Each host participating in the GlusterFS +cluster must define this type on itself, and on every other host. As a result, +this is not a singleton like the [gluster::server](#glusterserver) class. + +####`ip` +Specify which IP address this host is using. This defaults to the +_$::ipaddress_ variable. Be sure to set this manually if you're declaring this +yourself on each host without using exported resources. If each host thinks the +other hosts should have the same IP address as itself, then Puppet-Gluster and +GlusterFS won't work correctly. + +####`uuid` +Universally unique identifier (UUID) for the host. If empty, Puppet-Gluster +will generate this automatically for the host. You can generate your own +manually with _uuidgen_, and set them yourself. I found this particularly +useful for testing, because I would pick easy to recognize UUID's like: +_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa_, +_bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb_, and so on. If you set a UUID manually, +and Puppet-Gluster has a chance to run, then it will remember your choice, and +store it locally to be used again if you no longer specify the UUID. This is +particularly useful for upgrading an existing un-managed GlusterFS installation +to a Puppet-Gluster managed one, without changing any UUID's. + +###gluster::brick +Main brick type for the cluster. Each brick is an individual storage segment to +be used on a host. Each host must have at least one brick to participate in the +cluster, but usually a host will have multiple bricks. A brick can be as simple +as a file system folder, or it can be a separate file system. Please read the +official GlusterFS documentation, if you aren't entirely comfortable with the +concept of a brick. + +For most test clusters, and for experimentation, it is easiest to use a +directory on the root file system. You can even use a _/tmp_ sub folder if you +don't care about the persistence of your data. For more serious clusters, you +might want to create separate file systems for your data. On self-hosted iron, +it is not uncommon to create multiple RAID-6 drive pools, and to then create a +separate file system per virtual drive. Each file system can then be used as a +single brick. + +So that each volume in GlusterFS has the maximum ability to grow, without +having to partition storage separately, the bricks in Puppet-Gluster are +actually folders (on whatever backing store you wish) which then contain +sub folders-- one for each volume. As a result, all the volumes on a given +GlusterFS cluster can share the total available storage space. If you wish to +limit the storage used by each volume, you can setup quotas. Alternatively, you +can buy more hardware, and elastically grow your GlusterFS volumes, since the +price per GB will be significantly less than any proprietary storage system. +The one downside to this brick sharing, is that if you have chosen the brick +per host count specifically to match your performance requirements, and +each GlusterFS volume on the same cluster has drastically different brick per +host performance requirements, then this won't suit your needs. I doubt that +anyone actually has such requirements, but if you do insist on needing this +compartmentalization, then you can probably use the Puppet-Gluster grouping +feature to accomplish this goal. Please let me know about your use-case, and +be warned that the grouping feature hasn't been extensively tested. + +To prove to you that I care about automation, this type offers the ability to +automatically partition and format your file systems. This means you can plug +in new iron, boot, provision and configure the entire system automatically. +Regrettably, I don't have a lot of test hardware to routinely use this feature. +If you'd like to donate some, I'd be happy to test this thoroughly. Having said +that, I have used this feature, I consider it to be extremely safe, and it has +never caused me to lose data. If you're uncertain, feel free to look at the +code, or avoid using this feature entirely. If you think there's a way to make +it even safer, then feel free to let me know. + +####`dev` +Block device, such as _/dev/sdc_ or _/dev/disk/by-id/scsi-0123456789abcdef_. By +default, Puppet-Gluster will assume you're using a folder to store the brick +data, if you don't specify this parameter. + +####`raid_su` +Get this information from your RAID device. This is used to do automatic +calculations for alignment, so that the: + +``` + dev -> part -> lvm -> fs +``` + +stack is aligned properly. Future work is possible to manage your RAID devices, +and to read these values automatically. Specify this value as an integer number +of kilobytes (k). + +####`raid_sw` +Get this information from your RAID device. This is used to do automatic +calculations for alignment, so that the: + +``` + dev -> part -> lvm -> fs +``` + +stack is aligned properly. Future work is possible to manage your RAID devices, +and to read these values automatically. Specify this value as an integer. + +####`partition` +Do you want to partition the device and build the next layer on that partition, +or do you want to build on the block device directly? The "next layer" will +typically be lvm if you're using lvm, or your file system (such as xfs) if +you're skipping the lvm layer. + +####`labeltype` +Only _gpt_ is supported. Other options include _msdos_, but this has never been +used because of it's size limitations. + +####`lvm` +Do you want to use lvm on the lower level device (typically a partition, or the +device itself), or not. Using lvm might be required when using a commercially +supported GlusterFS solution. + +####`lvm_thinp` +Set to _true_ to enable LVM thin provisioning. Read 'man 7 lvmthin' to +understand what thin provisioning is all about. This is needed for one form of +GlusterFS snapshots. Obviously this requires that you also enable _LVM_. + +####`lvm_virtsize` +The value that will be passed to _--virtualsize_. By default this will pass in +a command that will return the size of your volume group. This is usually a +sane value, and help you to remember not to overcommit. + +####`lvm_chunksize` +Value of _--chunksize_ for _lvcreate_ when using thin provisioning. + +####`lvm_metadatasize` +Value of _--poolmetadatasize_ for _lvcreate_ when using thin provisioning. + +####`fsuuid` +File system UUID. This ensures we can distinctly identify a file system. You +can set this to be used with automatic file system creation, or you can specify +the file system UUID that you'd like to use. If you leave this blank, then +Puppet-Gluster can automatically pick an fs UUID for you. This is especially +useful if you are automatically deploying a large cluster on physical iron. + +####`fstype` +This should be _xfs_ or _ext4_. Using _xfs_ is recommended, but _ext4_ is also +quite common. This only affects a file system that is getting created by this +module. If you provision a new machine, with a root file system of _ext4_, and +the brick you create is a root file system path, then this option does nothing. +A _btrfs_ option is now available for testing. It is not officially supported +by GlusterFS, but testing it anyways, and reporting any issues is encouraged. + +####`xfs_inode64` +Set _inode64_ mount option when using the _xfs_ fstype. Choose _true_ to set. + +####`xfs_nobarrier` +Set _nobarrier_ mount option when using the _xfs_ fstype. Choose _true_ to set. + +####`ro` +Whether the file system should be mounted read only. For emergencies only. + +####`force` +If _true_, this will overwrite any xfs file system it sees. This is useful for +rebuilding GlusterFS repeatedly and wiping data. There are other safeties in +place to stop this. In general, you probably don't ever want to touch this. + +####`areyousure` +Do you want to allow Puppet-Gluster to do dangerous things? You have to set +this to _true_ to allow Puppet-Gluster to _fdisk_ and _mkfs_ your file system. + +####`again` +Do you want to use _Exec['again']_ ? This helps build your cluster quickly! + +####`comment` +Add any comment you want. This is also occasionally used internally to do magic +things. + +###gluster::volume +Main volume type for the cluster. This is where a lot of the magic happens. +Remember that changing some of these parameters after the volume has been +created won't work, and you'll experience undefined behaviour. There could be +FSM based error checking to verify that no changes occur, but it has been left +out so that this code base can eventually support such changes, and so that the +user can manually change a parameter if they know that it is safe to do so. + +####`bricks` +List of bricks to use for this volume. If this is left at the default value of +_true_, then this list is built automatically. The algorithm that determines +this order does not support all possible situations, and most likely can't +handle certain corner cases. It is possible to examine the FSM to view the +selected brick order before it has a chance to create the volume. The volume +creation script won't run until there is a stable brick list as seen by the FSM +running on the host that has the DLM. If you specify this list of bricks +manually, you must choose the order to match your desired volume layout. If you +aren't sure about how to order the bricks, you should review the GlusterFS +documentation first. + +####`transport` +Only _tcp_ is supported. Possible values can include _rdma_, but this won't get +any testing if I don't have access to infiniband hardware. Donations welcome. + +####`replica` +Replica count. Usually you'll want to set this to _2_. Some users choose _3_. +Other values are seldom seen. A value of _1_ can be used for simply testing a +distributed setup, when you don't care about your data or high availability. A +value greater than _4_ is probably wasteful and unnecessary. It might even +cause performance issues if a synchronous write is waiting on a slow fourth +server. + +####`stripe` +Stripe count. Thoroughly unsupported and untested option. Not recommended for +use by GlusterFS. + +####`layout` +Which brick layout to use. The available options are: _chained_, and (default). +To generate a default (symmetrical, balanced) layout, leave this option blank. +If you'd like to include an algorithm that generates a different type of brick +layout, it is easy to drop in an algorithm. Please contact me with the details! + +####`ping` +Do we want to include ping checks with _fping_? + +####`settle` +Do we want to run settle checks? + +####`again` +Do you want to use _Exec['again']_ ? This helps build your cluster quickly! + +####`start` +Requested state for the volume. Valid values include: _true_ (start), _false_ +(stop), or _undef_ (un-managed start/stop state). + +###gluster::volume::property +Main volume property type for the cluster. This allows you to manage GlusterFS +volume specific properties. There are a wide range of properties that volumes +support. For the full list of properties, you should consult the GlusterFS +documentation, or run the _gluster volume set help_ command. To set a property +you must use the special name pattern of: _volume_#_key_. The value argument is +used to set the associated value. It is smart enough to accept values in the +most logical format for that specific property. Some properties aren't yet +supported, so please report any problems you have with this functionality. +Because this feature is an awesome way to _document as code_ the volume +specific optimizations that you've made, make sure you use this feature even if +you don't use all the others. + +####`value` +The value to be used for this volume property. + +###gluster::mount +Main type to use to mount GlusterFS volumes. This type offers special features, +like shorewall integration, and repo support. + +####`server` +Server specification to use when mounting. Format is _:/volume_. You +may use an _FQDN_ or an _IP address_ to specify the server. + +####`rw` +Mount read-write or read-only. Defaults to read-only. Specify _true_ for +read-write. + +####`mounted` +Mounted argument from standard mount type. Defaults to _true_ (_mounted_). + +####`repo` +Boolean to select if you want automatic repository (package) management or not. + +####`version` +Specify which GlusterFS version you'd like to use. + +####`ip` +IP address of this client. This is usually auto-detected, but you can choose +your own value manually in case there are multiple options available. + +####`shorewall` +Boolean to specify whether puppet-shorewall integration should be used or not. + +##Examples +For example configurations, please consult the [examples/](https://github.com/purpleidea/puppet-gluster/tree/master/examples) directory in the git +source repository. It is available from: + +[https://github.com/purpleidea/puppet-gluster/tree/master/examples](https://github.com/purpleidea/puppet-gluster/tree/master/examples) + +It is also available from: + +[https://forge.gluster.org/puppet-gluster/puppet-gluster/trees/master/examples](https://forge.gluster.org/puppet-gluster/puppet-gluster/trees/master/examples/) + +##Limitations + +This module has been tested against open source Puppet 3.2.4 and higher. + +The module is routinely tested on: + +* CentOS 6.5 + +It will probably work without incident or without major modification on: + +* CentOS 5.x/6.x +* RHEL 5.x/6.x + +It has patches to support: + +* Fedora 20+ +* Ubuntu 12.04+ +* Debian 7+ + +It will most likely work with other Puppet versions and on other platforms, but +testing on those platforms has been minimal due to lack of time and resources. + +Testing is community supported! Please report any issues as there are a lot of +features, and in particular, support for additional distros isn't well tested. +The multi-distro architecture has been chosen to easily support new additions. +Most platforms and versions will only require a change to the yaml based data/ +folder. + +##Development + +This is my personal project that I work on in my free time. +Donations of funding, hardware, virtual machines, and other resources are +appreciated. Please contact me if you'd like to sponsor a feature, invite me to +talk/teach or for consulting. + +You can follow along [on my technical blog](https://ttboj.wordpress.com/). + +To report any bugs, please file a ticket at: [https://bugzilla.redhat.com/enter_bug.cgi?product=GlusterFS&component=puppet-gluster](https://bugzilla.redhat.com/enter_bug.cgi?product=GlusterFS&component=puppet-gluster). + +##Author + +Copyright (C) 2010-2013+ James Shubin + +* [github](https://github.com/purpleidea/) +* [@purpleidea](https://twitter.com/#!/purpleidea) +* [https://ttboj.wordpress.com/](https://ttboj.wordpress.com/) + diff --git a/gluster/INSTALL b/gluster/INSTALL new file mode 100644 index 000000000..f497841f7 --- /dev/null +++ b/gluster/INSTALL @@ -0,0 +1,18 @@ +To install this puppet module, copy this folder to your puppet modulepath. + +You can usually find out where this is by running: + +$ puppet config print modulepath + +on your puppetmaster. In my case, this contains the directory: + +/etc/puppet/modules/ + +I keep all of my puppet modules in git managed directories named: + +puppet- + +You must remove the 'puppet-' prefix from the directory name for it to work! + +Happy hacking! + diff --git a/gluster/Makefile b/gluster/Makefile new file mode 100644 index 000000000..bb98acf00 --- /dev/null +++ b/gluster/Makefile @@ -0,0 +1,146 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +.PHONY: all docs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms +.SILENT: + +# version of the program +VERSION := $(shell cat VERSION) +RELEASE = 1 +SPEC = rpmbuild/SPECS/puppet-gluster.spec +SOURCE = rpmbuild/SOURCES/puppet-gluster-$(VERSION).tar.bz2 +SRPM = rpmbuild/SRPMS/puppet-gluster-$(VERSION)-$(RELEASE).src.rpm +RPM = rpmbuild/RPMS/puppet-gluster-$(VERSION)-$(RELEASE).rpm +SERVER = 'download.gluster.org' +REMOTE_PATH = 'purpleidea/puppet-gluster' + +all: docs rpm + +docs: puppet-gluster-documentation.pdf + +puppet-gluster-documentation.pdf: DOCUMENTATION.md + pandoc DOCUMENTATION.md -o 'puppet-gluster-documentation.pdf' + +# +# aliases +# +# TODO: does making an rpm depend on making a .srpm first ? +rpm: $(SRPM) $(RPM) + # do nothing + +srpm: $(SRPM) + # do nothing + +spec: $(SPEC) + # do nothing + +tar: $(SOURCE) + # do nothing + +upload: upload-sources upload-srpms upload-rpms + # do nothing + +# +# rpmbuild +# +$(RPM): $(SPEC) $(SOURCE) + @echo Running rpmbuild -bb... + rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bb $(SPEC) && \ + mv rpmbuild/RPMS/noarch/puppet-gluster-$(VERSION)-$(RELEASE).*.rpm $(RPM) + +$(SRPM): $(SPEC) $(SOURCE) + @echo Running rpmbuild -bs... + rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bs $(SPEC) + # renaming is not needed because we aren't using the dist variable + #mv rpmbuild/SRPMS/puppet-gluster-$(VERSION)-$(RELEASE).*.src.rpm $(SRPM) + +# +# spec +# +$(SPEC): rpmbuild/ puppet-gluster.spec.in + @echo Running templater... + #cat puppet-gluster.spec.in > $(SPEC) + sed -e s/__VERSION__/$(VERSION)/ -e s/__RELEASE__/$(RELEASE)/ < puppet-gluster.spec.in > $(SPEC) + # append a changelog to the .spec file + git log --format="* %cd %aN <%aE>%n- (%h) %s%d%n" --date=local | sed -r 's/[0-9]+:[0-9]+:[0-9]+ //' >> $(SPEC) + +# +# archive +# +$(SOURCE): rpmbuild/ + @echo Running git archive... + # use HEAD if tag doesn't exist yet, so that development is easier... + git archive --prefix=puppet-gluster-$(VERSION)/ -o $(SOURCE) $(VERSION) 2> /dev/null || (echo 'Warning: $(VERSION) does not exist.' && git archive --prefix=puppet-gluster-$(VERSION)/ -o $(SOURCE) HEAD) + +# TODO: ensure that each sub directory exists +rpmbuild/: + mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} + +# +# sha256sum +# +rpmbuild/SOURCES/SHA256SUMS: rpmbuild/SOURCES/ $(SOURCE) + @echo Running SOURCES sha256sum... + cd rpmbuild/SOURCES/ && sha256sum *.tar.bz2 > SHA256SUMS; cd - + +rpmbuild/SRPMS/SHA256SUMS: rpmbuild/SRPMS/ $(SRPM) + @echo Running SRPMS sha256sum... + cd rpmbuild/SRPMS/ && sha256sum *src.rpm > SHA256SUMS; cd - + +rpmbuild/RPMS/SHA256SUMS: rpmbuild/RPMS/ $(RPM) + @echo Running RPMS sha256sum... + cd rpmbuild/RPMS/ && sha256sum *.rpm > SHA256SUMS; cd - + +# +# gpg +# +rpmbuild/SOURCES/SHA256SUMS.asc: rpmbuild/SOURCES/SHA256SUMS + @echo Running SOURCES gpg... + # the --yes forces an overwrite of the SHA256SUMS.asc if necessary + gpg2 --yes --clearsign rpmbuild/SOURCES/SHA256SUMS + +rpmbuild/SRPMS/SHA256SUMS.asc: rpmbuild/SRPMS/SHA256SUMS + @echo Running SRPMS gpg... + gpg2 --yes --clearsign rpmbuild/SRPMS/SHA256SUMS + +rpmbuild/RPMS/SHA256SUMS.asc: rpmbuild/RPMS/SHA256SUMS + @echo Running RPMS gpg... + gpg2 --yes --clearsign rpmbuild/RPMS/SHA256SUMS + +# +# upload +# +# upload to public server +upload-sources: rpmbuild/SOURCES/ rpmbuild/SOURCES/SHA256SUMS rpmbuild/SOURCES/SHA256SUMS.asc + if [ "`cat rpmbuild/SOURCES/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/SOURCES/ && cat SHA256SUMS'`" ]; then \ + echo Running SOURCES upload...; \ + rsync -avz rpmbuild/SOURCES/ $(SERVER):$(REMOTE_PATH)/SOURCES/; \ + fi + +upload-srpms: rpmbuild/SRPMS/ rpmbuild/SRPMS/SHA256SUMS rpmbuild/SRPMS/SHA256SUMS.asc + if [ "`cat rpmbuild/SRPMS/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/SRPMS/ && cat SHA256SUMS'`" ]; then \ + echo Running SRPMS upload...; \ + rsync -avz rpmbuild/SRPMS/ $(SERVER):$(REMOTE_PATH)/SRPMS/; \ + fi + +upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.asc + if [ "`cat rpmbuild/RPMS/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/RPMS/ && cat SHA256SUMS'`" ]; then \ + echo Running RPMS upload...; \ + rsync -avz --prune-empty-dirs rpmbuild/RPMS/ $(SERVER):$(REMOTE_PATH)/RPMS/; \ + fi + +# vim: ts=8 diff --git a/gluster/Modulefile b/gluster/Modulefile new file mode 100644 index 000000000..2eaa0ab68 --- /dev/null +++ b/gluster/Modulefile @@ -0,0 +1,12 @@ +name 'purpleidea-gluster' +author 'James Shubin' +license 'GNU Affero General Public License, Version 3.0+' +summary 'GlusterFS module by James' +description 'A Puppet module for GlusterFS' +project_page 'https://github.com/purpleidea/puppet-gluster/' +source 'https://ttboj.wordpress.com/' +version '0.0.4' + +## Add dependencies, if any: +# dependency 'username/name', '>= 1.2.0' + diff --git a/gluster/README b/gluster/README new file mode 100644 index 000000000..c9482ddbf --- /dev/null +++ b/gluster/README @@ -0,0 +1,34 @@ +This is puppet-gluster a puppet module for gluster. + +Please read the INSTALL file for instructions on getting this installed. +Look in the examples/ folder for usage. If none exist, please contribute one! +This code may be a work in progress. The interfaces may change without notice. +Patches are welcome, but please be patient. They are best received by email. +Please ping me if you have big changes in mind, before you write a giant patch. + +Module specific notes: +* This is _the_ puppet module for gluster. Accept no imitations! +* All the participating nodes, need to have an identical puppet-gluster config. +* Using gluster::simple is probably the best way to try this out. +* This is easily deployed with vagrant. See the vagrant/ directory! +* You can use less of the available resources, if you only want to manage some. +* You can get CentOS and RHEL rpms from: + * http://download.gluster.org/pub/gluster/glusterfs/LATEST/CentOS/ or: + * http://repos.fedorapeople.org/repos/kkeithle/glusterfs/epel-6/x86_64/ +* Documentation is now available! Please report grammar and spelling bugs. + +Dependencies: +* puppetlabs-stdlib (required) +* puppet-module-data (optional, puppet >= 3.0.0) +* my puppet-common module (optional) +* my puppet-shorewall module (optional) +* my puppet-keepalived module (optional) +* my puppet-puppet module (optional) +* my puppet-yum module (optional) +* gluster packages (see above notes) +* pandoc (for building a pdf of the documentation) + + +Happy hacking, +James Shubin , https://ttboj.wordpress.com/ + diff --git a/gluster/README.md b/gluster/README.md new file mode 120000 index 000000000..fc84f4f88 --- /dev/null +++ b/gluster/README.md @@ -0,0 +1 @@ +DOCUMENTATION.md \ No newline at end of file diff --git a/gluster/VERSION b/gluster/VERSION new file mode 100644 index 000000000..81340c7e7 --- /dev/null +++ b/gluster/VERSION @@ -0,0 +1 @@ +0.0.4 diff --git a/gluster/builder/README b/gluster/builder/README new file mode 100644 index 000000000..2bffb7126 --- /dev/null +++ b/gluster/builder/README @@ -0,0 +1,10 @@ +This folder (including history) has been split off into a separate repository. + +The new git repository is now located at: + + https://github.com/purpleidea/vagrant-builder + +Happy hacking! + +James / @purpleidea + diff --git a/gluster/data/common.yaml b/gluster/data/common.yaml new file mode 100644 index 000000000..ea6f232c8 --- /dev/null +++ b/gluster/data/common.yaml @@ -0,0 +1,5 @@ +# gluster/data/common.yaml +--- +gluster::params::comment: 'Hello from @purpleidea!' # do not erase! + +# vim: ts=8 diff --git a/gluster/data/hiera.yaml b/gluster/data/hiera.yaml new file mode 100644 index 000000000..52b83b57a --- /dev/null +++ b/gluster/data/hiera.yaml @@ -0,0 +1,9 @@ +# gluster/data/hiera.yaml +--- +:hierarchy: +- tree/%{::osfamily}/%{::operatingsystem}/%{::operatingsystemrelease} +- tree/%{::osfamily}/%{::operatingsystem} +- tree/%{::osfamily} +- common + +# vim: ts=8 diff --git a/gluster/data/tree/Debian.yaml b/gluster/data/tree/Debian.yaml new file mode 100644 index 000000000..098123cb9 --- /dev/null +++ b/gluster/data/tree/Debian.yaml @@ -0,0 +1,9 @@ +# gluster/data/tree/Debian.yaml +--- +gluster::params::package_glusterfs: 'glusterfs-client' +gluster::params::package_glusterfs_api: '' # doesn't exist +gluster::params::package_glusterfs_fuse: '' # doesn't exist +gluster::params::service_glusterd: 'glusterfs-server' +# TODO: the debian family of glusterd needs a reload command in the init file ! +gluster::params::misc_gluster_reload: '/usr/sbin/service glusterfs-server restart' +# vim: ts=8 diff --git a/gluster/data/tree/Debian/Debian.yaml b/gluster/data/tree/Debian/Debian.yaml new file mode 100644 index 000000000..b6d2ec32a --- /dev/null +++ b/gluster/data/tree/Debian/Debian.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/Debian/Debian.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/Debian/Debian/7.4.yaml b/gluster/data/tree/Debian/Debian/7.4.yaml new file mode 100644 index 000000000..94cbd639e --- /dev/null +++ b/gluster/data/tree/Debian/Debian/7.4.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/Debian/Debian/7.4.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/Debian/Ubuntu.yaml b/gluster/data/tree/Debian/Ubuntu.yaml new file mode 100644 index 000000000..4bfe9d769 --- /dev/null +++ b/gluster/data/tree/Debian/Ubuntu.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/Debian/Ubuntu.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/Debian/Ubuntu/12.04.yaml b/gluster/data/tree/Debian/Ubuntu/12.04.yaml new file mode 100644 index 000000000..e6c234a9c --- /dev/null +++ b/gluster/data/tree/Debian/Ubuntu/12.04.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/Debian/Ubuntu/12.04.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/Gentoo.yaml b/gluster/data/tree/Gentoo.yaml new file mode 100644 index 000000000..b1d63e788 --- /dev/null +++ b/gluster/data/tree/Gentoo.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/Gentoo.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/RedHat.yaml b/gluster/data/tree/RedHat.yaml new file mode 100644 index 000000000..f320de481 --- /dev/null +++ b/gluster/data/tree/RedHat.yaml @@ -0,0 +1,5 @@ +# gluster/data/tree/RedHat.yaml +--- +gluster::params::misc_gluster_repo: 'https://download.gluster.org/pub/gluster/glusterfs/' + +# vim: ts=8 diff --git a/gluster/data/tree/RedHat/CentOS.yaml b/gluster/data/tree/RedHat/CentOS.yaml new file mode 100644 index 000000000..d9ef302e0 --- /dev/null +++ b/gluster/data/tree/RedHat/CentOS.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/RedHat/CentOS.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/RedHat/CentOS/6.5.yaml b/gluster/data/tree/RedHat/CentOS/6.5.yaml new file mode 100644 index 000000000..06d5f62ce --- /dev/null +++ b/gluster/data/tree/RedHat/CentOS/6.5.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/RedHat/CentOS/6.5.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/RedHat/Fedora.yaml b/gluster/data/tree/RedHat/Fedora.yaml new file mode 100644 index 000000000..09a878955 --- /dev/null +++ b/gluster/data/tree/RedHat/Fedora.yaml @@ -0,0 +1,21 @@ +# gluster/data/tree/RedHat/Fedora.yaml +--- +gluster::params::package_python_argparse: '' # doesn't exist +gluster::params::program_modprobe: '/usr/sbin/modprobe' +gluster::params::program_lsmod: '/usr/sbin/lsmod' +gluster::params::program_parted: '/usr/sbin/parted' +gluster::params::program_pvcreate: '/usr/sbin/pvcreate' +gluster::params::program_vgcreate: '/usr/sbin/vgcreate' +gluster::params::program_lvcreate: '/usr/sbin/lvcreate' +gluster::params::program_vgs: '/usr/sbin/vgs' +gluster::params::program_lvs: '/usr/sbin/lvs' +gluster::params::program_pvdisplay: '/usr/sbin/pvdisplay' +gluster::params::program_vgdisplay: '/usr/sbin/vgdisplay' +#gluster::params::program_lvdisplay: '/usr/sbin/lvdisplay' +gluster::params::program_mkfs_xfs: '/usr/sbin/mkfs.xfs' +gluster::params::program_mkfs_ext4: '/usr/sbin/mkfs.ext4' +gluster::params::program_mkfs_btrfs: '/usr/sbin/mkfs.btrfs' +gluster::params::program_findmnt: '/usr/bin/findmnt' +gluster::params::misc_gluster_reload: '/usr/bin/systemctl reload glusterd' + +# vim: ts=8 diff --git a/gluster/data/tree/RedHat/Fedora/20.yaml b/gluster/data/tree/RedHat/Fedora/20.yaml new file mode 100644 index 000000000..e1320920d --- /dev/null +++ b/gluster/data/tree/RedHat/Fedora/20.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/RedHat/Fedora/20.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/RedHat/RedHat.yaml b/gluster/data/tree/RedHat/RedHat.yaml new file mode 100644 index 000000000..459369b40 --- /dev/null +++ b/gluster/data/tree/RedHat/RedHat.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/RedHat/RedHat.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/RedHat/RedHat/6.5.yaml b/gluster/data/tree/RedHat/RedHat/6.5.yaml new file mode 100644 index 000000000..a65e8f519 --- /dev/null +++ b/gluster/data/tree/RedHat/RedHat/6.5.yaml @@ -0,0 +1,4 @@ +# gluster/data/tree/RedHat/RedHat/6.5.yaml +--- + +# vim: ts=8 diff --git a/gluster/data/tree/RedHat/RedHat/7.0.yaml b/gluster/data/tree/RedHat/RedHat/7.0.yaml new file mode 100644 index 000000000..e7f0ae796 --- /dev/null +++ b/gluster/data/tree/RedHat/RedHat/7.0.yaml @@ -0,0 +1,6 @@ +# gluster/data/tree/RedHat/RedHat/7.0.yaml +--- +gluster::params::package_python_argparse: '' # doesn't exist +gluster::params::misc_gluster_reload: '/usr/bin/systemctl reload glusterd' + +# vim: ts=8 diff --git a/gluster/examples/distributed-replicate-example.pp b/gluster/examples/distributed-replicate-example.pp new file mode 100644 index 000000000..900d9caa7 --- /dev/null +++ b/gluster/examples/distributed-replicate-example.pp @@ -0,0 +1,154 @@ +# +# example of a distributed-replicate with four hosts, and two bricks each +# NOTE: this should be put on *every* gluster host +# +#$annex_loc_vip_1 = '172.16.1.80' # vip +$annex_loc_ip_1 = '172.16.1.81' +$annex_loc_ip_2 = '172.16.1.82' +$annex_loc_ip_3 = '172.16.1.83' +$annex_loc_ip_4 = '172.16.1.84' + +$some_client_ip = '' + +class gluster_base { + + class { 'gluster::server': + ips => ["${annex_loc_ip_1}", "${annex_loc_ip_2}", "${annex_loc_ip_3}", "${annex_loc_ip_4}"], + #vip => "${annex_loc_vip_1}", + clients => [$some_client_ip], + zone => 'loc', + shorewall => true, + } + + gluster::host { 'annex1.example.com': + # use uuidgen to make these + # NOTE: specifying a host uuid is optional and can be automatic + #uuid => '1f660ca2-2c78-4aa0-8f4d-21608218c69c', + } + + gluster::brick { 'annex1.example.com:/mnt/storage1a': + dev => '/dev/disk/by-id/scsi-36003048007e26c00173ad3b633a2ef67', # /dev/sda + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '1ae49642-7f34-4886-8d23-685d13867fb1', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, + } + + gluster::brick { 'annex1.example.com:/mnt/storage1c': + dev => '/dev/disk/by-id/scsi-36003048007e26c00173ad3b633a36700', # /dev/sdb + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '1c9ee010-9cd1-4d81-9a73-f0788d5b3e33', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, + } + + gluster::host { 'annex2.example.com': + uuid => '2fbe6e2f-f6bc-4c2d-a301-62fa90c459f8', + } + + gluster::brick { 'annex2.example.com:/mnt/storage2a': + dev => '/dev/disk/by-id/scsi-36003048007df450014ca27ee19eaec55', # /dev/sdc + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '2affe5e3-c10d-4d42-a887-cf8993a6c7b5', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, + } + + gluster::brick { 'annex2.example.com:/mnt/storage2c': + dev => '/dev/disk/by-id/scsi-36003048007df450014ca280e1bda8e70', # /dev/sdd + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '2c434d6c-7800-4eec-9121-483bee2336f6', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, + } + + gluster::host { 'annex3.example.com': + uuid => '3f5a86fd-6956-46ca-bb80-65278dc5b945', + } + + gluster::brick { 'annex3.example.com:/mnt/storage3b': + dev => '/dev/disk/by-id/scsi-36003048007e14f0014ca2722130bc87c', # /dev/sdc + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '3b79d76b-a519-493c-9f21-ca35524187ef', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, + } + + gluster::brick { 'annex3.example.com:/mnt/storage3d': + dev => '/dev/disk/by-id/scsi-36003048007e14f0014ca2743150a5471', # /dev/sdd + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '3d125f8a-c3c3-490d-a606-453401e9bc21', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, + } + + gluster::host { 'annex4.example.com': + uuid => '4f8e3157-e1e3-4f13-b9a8-51e933d53915', + } + + gluster::brick { 'annex4.example.com:/mnt/storage4b': + dev => '/dev/disk/by-id/scsi-36003048007e36700174029270d81faa1', # /dev/sdc + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '4bf21ae6-06a0-44ad-ab48-ea23417e4e44', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, + } + + gluster::brick { 'annex4.example.com:/mnt/storage4d': + dev => '/dev/disk/by-id/scsi-36003048007e36700174029270d82724d', # /dev/sdd + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '4dfa7e50-2315-44d3-909b-8e9423def6e5', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, + } + + $brick_list = [ + 'annex1.example.com:/mnt/storage1a', + 'annex2.example.com:/mnt/storage2a', + 'annex3.example.com:/mnt/storage3b', + 'annex4.example.com:/mnt/storage4b', + 'annex1.example.com:/mnt/storage1c', + 'annex2.example.com:/mnt/storage2c', + 'annex3.example.com:/mnt/storage3d', + 'annex4.example.com:/mnt/storage4d', + ] + + # TODO: have this run transactionally on *one* gluster host. + gluster::volume { 'examplevol': + replica => 2, + bricks => $brick_list, + start => undef, # i'll start this myself + } + + # namevar must be: # + gluster::volume::property { 'examplevol#auth.reject': + value => ['192.0.2.13', '198.51.100.42', '203.0.113.69'], + } + + #gluster::volume::property { 'examplevol#cluster.data-self-heal-algorithm': + # value => 'full', + #} + + #gluster::volume { 'someothervol': + # replica => 2, + # bricks => $brick_list, + # start => undef, + #} + +} diff --git a/gluster/examples/filesystem-backed-bricks-example.pp b/gluster/examples/filesystem-backed-bricks-example.pp new file mode 100644 index 000000000..89b77719d --- /dev/null +++ b/gluster/examples/filesystem-backed-bricks-example.pp @@ -0,0 +1,52 @@ +# +# example of a simple replicate with 2 hosts, and filesystem path bricks +# NOTE: this should be put on *every* gluster host +# + +class gluster_base { + + class { '::gluster::server': + ips => ['192.168.123.101', '192.168.123.102'], + shorewall => true, + } + + gluster::host { 'annex1.example.com': + # use uuidgen to make these + uuid => '1f660ca2-2c78-4aa0-8f4d-21608218c69c', + } + + # note that this is using a folder on your existing filesystem... + # this can be useful for prototyping gluster using virtual machines + # if this isn't a separate partition, remember that your root fs will + # run out of space when your gluster volume does! + gluster::brick { 'annex1.example.com:/data/gluster-storage1': + areyousure => true, + } + + gluster::host { 'annex2.example.com': + # NOTE: specifying a host uuid is now optional! + # if you don't choose one, one will be assigned + #uuid => '2fbe6e2f-f6bc-4c2d-a301-62fa90c459f8', + } + + gluster::brick { 'annex2.example.com:/data/gluster-storage2': + areyousure => true, + } + + $brick_list = [ + 'annex1.example.com:/data/gluster-storage1', + 'annex2.example.com:/data/gluster-storage2', + ] + + gluster::volume { 'examplevol': + replica => 2, + bricks => $brick_list, + start => undef, # i'll start this myself + } + + # namevar must be: # + gluster::volume::property { 'examplevol#auth.reject': + value => ['192.0.2.13', '198.51.100.42', '203.0.113.69'], + } +} + diff --git a/gluster/examples/gluster-nfs-ipa-example.pp b/gluster/examples/gluster-nfs-ipa-example.pp new file mode 100644 index 000000000..b08c7f2c4 --- /dev/null +++ b/gluster/examples/gluster-nfs-ipa-example.pp @@ -0,0 +1,47 @@ +# gluster::mount example using puppet-nfs and puppet-ipa to serve up your data! +# NOTE: you'll need to consult puppet-ipa/examples/ to setup the freeipa server + +# mount a share on your nfs server, at the moment that nfs server is a SPOF :-( +$gvip = '203.0.113.42' +gluster::mount { '/export/homes': + server => "${gvip}:/homes", + rw => true, + mounted => true, + require => Gluster::Volume['homes'], # TODO: too bad this can't ensure it's started +} + +class { '::nfs::server': + domain => "${::domain}", + ipa => 'nfs', # the ipa::client::service name + kerberos => 'ipa', # optional when we're using ipa + shorewall => true, +} + +# the $name here is the client mountpoint when we use: safety => false! +nfs::server::export { '/homes/': # name is the client mountpoint + export => '/export/homes/', + rw => true, + async => false, + wdelay => true, # if false then async must be false too + rootsquash => true, + sec => true, # set true for automatic kerberos magic + options => [], # add any other options you might want! + hosts => ["ws*.${domain}"], # export to these hosts only... + exported => true, # create exported resources for clients + tagas => 'homes', + safety => false, # be super clever (see the module docs) + comment => 'Export home directories for ws*', + require => Gluster::Mount['/export/homes/'], +} + +# and here is how you can collect / mount ~automatically on the client: +class { '::nfs::client': + kerberos => true, +} + +nfs::client::mount::collect { 'homes': # match the $tagas from export! + server => "${::hostname}.${::domain}", + #suid => false, + #clientaddr => "${::ipaddress}", # use this if you want! +} + diff --git a/gluster/examples/gluster-simple-example.pp b/gluster/examples/gluster-simple-example.pp new file mode 100644 index 000000000..02e59f3ab --- /dev/null +++ b/gluster/examples/gluster-simple-example.pp @@ -0,0 +1,17 @@ +# +# simple gluster setup. yeah, that's it. +# this should run on *every* gluster host +# NOTE: this should use a VIP. +# + +node /^annex\d+$/ { # annex{1,2,..N} + + # NOTE: this class offers some configuration, see the source for info. + # NOTE: this is mostly intended for fast gluster testing. for more + # complex setups, you might want to look at the other examples. + class { '::gluster::simple': + setgroup => 'virt', # or: 'small-file-perf', or others too! + } + +} + diff --git a/gluster/examples/gluster-simple-physical-example-best.pp b/gluster/examples/gluster-simple-physical-example-best.pp new file mode 100644 index 000000000..06be1eda1 --- /dev/null +++ b/gluster/examples/gluster-simple-physical-example-best.pp @@ -0,0 +1,28 @@ +# +# really simple gluster setup for physical provisioning. +# (yeah, that's it-- for iron!) +# + +node /^annex\d+$/ { # annex{1,2,..N} + + class { '::gluster::simple': + replica => 2, + vip = '192.168.1.42', + vrrp = true, + # NOTE: _each_ host will have four bricks with these devices... + brick_params_defaults = [ # note the spelling and type... + {'dev' => '/dev/sdb'}, + {'dev' => '/dev/sdc'}, + {'dev' => '/dev/sdd'}, + {'dev' => '/dev/sde'}, + ], + brick_param_defaults => { # every brick will use these... + lvm => false, + xfs_inode64 => true, + force => true, + }, + #brick_params => {}, # NOTE: you can also use this option to + # override a particular fqdn with the options that you need to! + } +} + diff --git a/gluster/examples/gluster-simple-physical-example.pp b/gluster/examples/gluster-simple-physical-example.pp new file mode 100644 index 000000000..377958bb4 --- /dev/null +++ b/gluster/examples/gluster-simple-physical-example.pp @@ -0,0 +1,40 @@ +# +# simple gluster setup for physical provisioning. +# (yeah, that's it-- for iron!) +# + +node /^annex\d+$/ { # annex{1,2,..N} + + class { '::gluster::simple': + # by allowing you to enumerate these things here in this class, + # you're able to specify all of these from a provisioning tool. + # this is useful in a tool like foreman which only lets you set + # class variables, and doesn't let you define individual types! + replica => 2, + vip = '192.168.1.42', + vrrp = true, + # NOTE: this example will show you different possibilities, but + # it is probably not sane to define your devices in a mixed way + brick_params => { + 'fqdn1.example.com' => [ + {dev => '/dev/disk/by-uuid/01234567-89ab-cdef-0123-456789abcdef'}, + {dev => '/dev/sde', partition => false}, + ], + 'fqdn2.example.com' => [ + {dev => '/dev/disk/by-path/pci-0000:02:00.0-scsi-0:1:0:0', raid_su => 256, raid_sw => 10} + {dev => '/dev/disk/by-id/wwn-0x600508e0000000002b012b744715743a', lvm => true}, + ], + #'fqdnN.example.com' => [...], + }, + + # these will get used by every brick, even if only specified by + # the count variable... keep in mind that without the $dev var, + # some of these parameters aren't used by the filesystem brick. + brick_param_defaults => { + lvm => false, + xfs_inode64 => true, + force => true, + }, + } +} + diff --git a/gluster/examples/mount-example.pp b/gluster/examples/mount-example.pp new file mode 100644 index 000000000..90930e32a --- /dev/null +++ b/gluster/examples/mount-example.pp @@ -0,0 +1,21 @@ +# gluster::mount example +# This is the recommended way of mounting puppet-gluster. +# NOTE: It makes sense to use the VIP as the server to mount from, since it +# stays HA if one of the other nodes goes down. + +# mount a share on one of the gluster hosts (note the added require) +$annex_loc_vip_1 = '172.16.1.80' +gluster::mount { '/mnt/gshared': + server => "${annex_loc_vip_1}:/gshared", + rw => true, + mounted => true, + require => Gluster::Volume['gshared'], # TODO: too bad this can't ensure it's started +} + +# mount a share on a client somewhere +gluster::mount { '/mnt/some_mount_point': + server => "${annex_loc_vip_1}:/some_volume_name", + rw => true, + mounted => true, +} + diff --git a/gluster/examples/wrapper-example.pp b/gluster/examples/wrapper-example.pp new file mode 100644 index 000000000..bae6614a9 --- /dev/null +++ b/gluster/examples/wrapper-example.pp @@ -0,0 +1,152 @@ +# gluster::wrapper example +# This is the recommended way of using puppet-gluster. +# NOTE: I have broken down the trees into pieces to make them easier to read. +# You can do it exactly like this, use giant trees, or even generate the tree +# using your favourite puppet tool. +# NOTE: These tree objects are actually just nested ruby hashes. + +class { 'gluster::wrapper': + nodetree => $nodetree, + volumetree => $volumetree, + # NOTE: this is the virtual ip as managed by keepalived. At this time, + # you must set up this part on your own. Using the VIP is recommended. + # NOTE: you can set this to any of the node ip's to manage puppet from + # a single master, or you can leave it blank to get the nodes to race. + vip => '172.16.1.80', +} + +$brick1a = { + dev => '/dev/disk/by-id/scsi-36003048007e26c00173ad3b633a2ef67', # /dev/sda + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '1ae49642-7f34-4886-8d23-685d13867fb1', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, +} + +$brick1c = { + dev => '/dev/disk/by-id/scsi-36003048007e26c00173ad3b633a36700', # /dev/sdb + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '1c9ee010-9cd1-4d81-9a73-f0788d5b3e33', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, +} + +$brick2a = { + dev => '/dev/disk/by-id/scsi-36003048007df450014ca27ee19eaec55', # /dev/sdc + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '2affe5e3-c10d-4d42-a887-cf8993a6c7b5', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, +} + +$brick2c = { + dev => '/dev/disk/by-id/scsi-36003048007df450014ca280e1bda8e70', # /dev/sdd + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '2c434d6c-7800-4eec-9121-483bee2336f6', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, +} + +$brick3b = { + dev => '/dev/disk/by-id/scsi-36003048007e14f0014ca2722130bc87c', # /dev/sdc + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '3b79d76b-a519-493c-9f21-ca35524187ef', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, +} + +$brick3d = { + dev => '/dev/disk/by-id/scsi-36003048007e14f0014ca2743150a5471', # /dev/sdd + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '3d125f8a-c3c3-490d-a606-453401e9bc21', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, +} + +$brick4b = { + dev => '/dev/disk/by-id/scsi-36003048007e36700174029270d81faa1', # /dev/sdc + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '4bf21ae6-06a0-44ad-ab48-ea23417e4e44', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, +} + +$brick4d = { + dev => '/dev/disk/by-id/scsi-36003048007e36700174029270d82724d', # /dev/sdd + labeltype => 'gpt', + fstype => 'xfs', + fsuuid => '4dfa7e50-2315-44d3-909b-8e9423def6e5', + xfs_inode64 => true, + xfs_nobarrier => true, + areyousure => true, +} + +$nodetree = { + 'annex1.example.com' => { + 'ip' => '172.16.1.81', + 'uuid' => '1f660ca2-2c78-4aa0-8f4d-21608218c69c', + 'bricks' => { + '/mnt/storage1a' => $brick1a, + '/mnt/storage1c' => $brick1c, + }, + }, + 'annex2.example.com' => { + 'ip' => '172.16.1.82', + 'uuid' => '2fbe6e2f-f6bc-4c2d-a301-62fa90c459f8', + 'bricks' => { + '/mnt/storage2a' => $brick2a, + '/mnt/storage2c' => $brick2c, + }, + }, + 'annex3.example.com' => { + 'ip' => '172.16.1.83', + 'uuid' => '3f5a86fd-6956-46ca-bb80-65278dc5b945', + 'bricks' => { + '/mnt/storage3b' => $brick3b, + '/mnt/storage3d' => $brick3d, + }, + }, + 'annex4.example.com' => { + 'ip' => '172.16.1.84', + 'uuid' => '4f8e3157-e1e3-4f13-b9a8-51e933d53915', + 'bricks' => { + '/mnt/storage4b' => $brick4b, + '/mnt/storage4d' => $brick4d, + }, + } +} + +$volumetree = { + 'examplevol1' => { + 'transport' => 'tcp', + 'replica' => 2, + 'clients' => ['172.16.1.143'], # for the auth.allow and $FW + # NOTE: if you *don't* specify a bricks argument, the full list + # of bricks above will be used for your new volume. This is the + # usual thing that you want to do. Alternatively you can choose + # your own bricks[] if you're doing something special or weird! + #'bricks' => [], + }, + + 'examplevol2' => { + 'transport' => 'tcp', + 'replica' => 2, + 'clients' => ['172.16.1.143', '172.16.1.253'], + #'bricks' => [], + } +} + diff --git a/gluster/files/groups/small-file-perf b/gluster/files/groups/small-file-perf new file mode 100644 index 000000000..a718522cf --- /dev/null +++ b/gluster/files/groups/small-file-perf @@ -0,0 +1,2 @@ +quick-read=on +open-behind=on diff --git a/gluster/files/groups/virt b/gluster/files/groups/virt new file mode 100644 index 000000000..0abe9f400 --- /dev/null +++ b/gluster/files/groups/virt @@ -0,0 +1,8 @@ +quick-read=off +read-ahead=off +io-cache=off +stat-prefetch=off +eager-lock=enable +remote-dio=enable +quorum-type=auto +server-quorum-type=server diff --git a/gluster/files/sponge.py b/gluster/files/sponge.py new file mode 100755 index 000000000..3ed44db5d --- /dev/null +++ b/gluster/files/sponge.py @@ -0,0 +1,44 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# This is meant to be a replacement for the excellent 'sponge' utility from the +# 'moreutils' package by Joey Hess. It doesn't do exactly what sponge does, but +# it does to the extent of what is used here for this Puppet-Gluster module. It +# is useful for environments that do not have the sponge package in their repo. + +import sys + +x = sys.stdin.readlines() +# TODO: can we be certain to sync here, and that the compiler doesn't optimize? + +if len(sys.argv) == 1: + # stdout + # TODO: untested + for l in x: sys.stdout.write(l) + sys.stdout.flush() + +elif len(sys.argv) == 2: + # file + f = open(sys.argv[1], 'w') + for l in x: f.write(l) + f.close() + +else: + sys.exit(1) + +# vim: ts=8 diff --git a/gluster/files/xml.py b/gluster/files/xml.py new file mode 100755 index 000000000..0650127f0 --- /dev/null +++ b/gluster/files/xml.py @@ -0,0 +1,261 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# EXAMPLE: +# $ gluster peer status --xml | ./xml.py connected +# + +# EXAMPLE: +# $ gluster peer status --xml | ./xml.py stuck +# + +# EXAMPLE: +# $ gluster volume info --xml | ./xml.py property --key +# + +# EXAMPLE: +# $ gluster volume status --xml [] | ./xml.py port --volume --host --path +# + +# EXAMPLE: +# $ gluster volume status --xml [] | ./xml.py ports [--volume ] [--host ] +# [,[,]] + +import sys +import argparse +import lxml.etree as etree + +# List of state codes: +# +# static char *glusterd_friend_sm_state_names[] = { # glusterd-sm.c +# "Establishing Connection", # 0 +# "Probe Sent to Peer", # 1 +# "Probe Received from Peer", # 2 +# "Peer in Cluster", # 3 (verified) +# "Accepted peer request", # 4 +# "Sent and Received peer request", # 5 +# "Peer Rejected", # 6 (verified) +# "Peer detach in progress", # 7 +# "Probe Received from peer", # 8 +# "Connected to Peer", # 9 +# "Peer is connected and Accepted", # 10 +# "Invalid State" # 11 +# }; +VALID_PEERED = ['3'] +VALID_STUCK = ['4'] + +parser = argparse.ArgumentParser(description='gluster xml parsing tools') +#parser.add_argument('--debug', dest='debug', action='store_true', default=False) +subparsers = parser.add_subparsers(dest='mode') + +# +# 'connected' parser +# +parser_connected = subparsers.add_parser('connected') +parser_connected.add_argument('peers', type=str, nargs='*', action='store') + +# +# 'stuck' parser +# +parser_stuck = subparsers.add_parser('stuck') +parser_stuck.add_argument('peers', type=str, nargs='*', action='store') + +# +# 'property' parser +# +parser_property = subparsers.add_parser('property') +parser_property.add_argument('--key', dest='key', action='store') + +# +# 'port' parser +# +parser_port = subparsers.add_parser('port') +parser_port.add_argument('--volume', dest='volume', action='store', required=True) +parser_port.add_argument('--host', dest='host', action='store', required=True) +parser_port.add_argument('--path', dest='path', action='store', required=True) + +# +# 'ports' parser +# +parser_ports = subparsers.add_parser('ports') +parser_ports.add_argument('--volume', dest='volume', action='store', required=False) +parser_ports.add_argument('--host', dest='host', action='store', required=False) + +# +# final setup... +# +args = parser.parse_args() +tree = etree.parse(sys.stdin) +root = tree.getroot() + +# are all the hostnames in argv connected ? +if args.mode == 'connected': + store = {} + peers = args.peers + + l = root.findall('.//peerStatus') + if len(l) != 1: + sys.exit(3) + + for p in l[0].findall('.//peer'): + h = p.find('hostname').text + c = (str(p.find('connected').text) == '1') # connected...? + s = (str(p.find('state').text) in VALID_PEERED) # valid peering + store[h] = c and s # save for later... + + # if no peers specified, assume we should check all... + if len(peers) == 0: + peers = store.keys() + + for i in peers: + if i in store.keys(): + if not store[i]: + # someone is unconnected + sys.exit(1) + else: + # we're looking for a peer that isn't peered yet + sys.exit(2) + + # must be good! + sys.exit(0) + +# are any hosts 'stuck' ? +elif args.mode == 'stuck': + store = {} + peers = args.peers + + l = root.findall('.//peerStatus') + if len(l) != 1: + sys.exit(3) + + for p in l[0].findall('.//peer'): + h = p.find('hostname').text + c = (str(p.find('connected').text) == '1') # connected...? + s = (str(p.find('state').text) in VALID_STUCK) # is it stuck ? + store[h] = c and s # save for later... + + # if no peers specified, assume we should check all... + if len(peers) == 0: + peers = store.keys() + + for i in peers: + if i in store.keys(): + if store[i]: + # someone is stuck + sys.exit(0) + else: + # we're looking for a peer that isn't peered yet + sys.exit(2) + + # nobody is stuck + sys.exit(1) + +elif args.mode == 'property': + store = [] + for i in root.findall('.//option'): + + key = str(i.find('name').text) + # if the key requested has no '.' in the name, we match loosely + if args.key.find('.') == -1: # no '.' have been found + # try and match key without a '.' in the name... + key = key[key.find('.')+1:] # remove prefix! + + if key == args.key: + store.append(i.find('value').text) + + if len(store) == 1: + print(store[0]) + sys.exit(0) + else: # more than one value found + sys.exit(1) + +elif args.mode == 'port': + port = 0 + found = False + #print args.volume # volume + #print args.host # hostname + #print args.path # path + for i in root.findall('.//volumes'): + for j in i.findall('.//volume'): + v = str(j.find('volName').text) + #print v + for k in j.findall('.//node'): + if k.find('node') is not None: + # this is a node in a node + # the NFS Server entry is doing + # doing this and might be a bug + continue + + h = str(k.find('hostname').text) + p = str(k.find('path').text) + #print h, p + #if v == args.volume and h == args.host and p == args.path: + if (v, h, p) == (args.volume, args.host, args.path): + if found: + # we have already found a match. + # there's a bug somewhere... + sys.exit(2) + found = True + port = int(k.find('port').text) + + if found and port > 0: + print(port) + sys.exit(0) + else: # no value found + sys.exit(1) + +# list all the ports used by one volume +elif args.mode == 'ports': + ports = [] + found = False + #print args.volume # volume (optional) + for i in root.findall('.//volumes'): + for j in i.findall('.//volume'): + v = str(j.find('volName').text) + #print v + # if no volume is specified, we use all of them... + if args.volume is None or args.volume == v: + for k in j.findall('.//node'): + if k.find('node') is not None: + # this is a node in a node + # the NFS Server entry is doing + # doing this and might be a bug + continue + + h = str(k.find('hostname').text) + p = str(k.find('path').text) + #print h, p + if args.host is None or args.host == h: + try: + ports.append(int(k.find('port').text)) + found = True + except ValueError, e: + pass + + if found and len(ports) > 0: + # NOTE: you may get duplicates if you lookup multiple hosts... + # here we remove any duplicates and convert each int to strings + print(','.join([str(x) for x in list(set(ports))])) + sys.exit(0) + else: # no value found + sys.exit(1) + +# else: +sys.exit(3) + +# vim: ts=8 diff --git a/gluster/lib/facter/gluster_bricks.rb b/gluster/lib/facter/gluster_bricks.rb new file mode 100644 index 000000000..e258fdd79 --- /dev/null +++ b/gluster/lib/facter/gluster_bricks.rb @@ -0,0 +1,104 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' + +# regexp to match a brick pattern, eg: annex1.example.com:/storage1a +regexp = /^[a-zA-Z]{1}[a-zA-Z0-9\.\-]{0,}:\/[a-zA-Z0-9]{1}[a-zA-Z0-9\/\.\-]{0,}$/ # TODO: is this right ? + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't continue + valid_brickdir = nil +else + module_vardir = var+'gluster/' + valid_brickdir = module_vardir.gsub(/\/$/, '')+'/brick/' +end + +found = {} +result = {} + +if not(valid_brickdir.nil?) and File.directory?(valid_brickdir) + Dir.glob(valid_brickdir+'*.*').each do |f| + b = File.basename(f) + g = b.split('.') # $name.group + + group = g.pop() # pop off suffix (the group name) + if not found.key?(group) + found[group] = [] # initialize + end + + if g.length >= 1 + x = g.join('.') # in case value had dots in it. + + brick = File.open(f, 'r').read.strip # read into str + # eg: annex1.example.com:/storage1a + split = brick.split(':') # do some $name parsing + host = split[0] # host fqdn + # NOTE: technically $path should be everything BUT split[0]. This + # lets our $path include colons if for some reason they're needed. + #path = split[1] # brick mount or storage path + path = brick.slice(host.length+1, brick.length-host.length-1) + + if brick.length > 0 and regexp.match(brick) + found[group].push({'host' => host, 'path' => path}) + # TODO: print warning on else... + end + end + end +end + +# transform to strings +found.keys.each do |group| + # build final result + result[group] = found[group].collect {|x| x['host']+':'+x['path'] } +end + +# build the correctly sorted brick list... +result.keys.each do |x| + Facter.add('gluster_brick_group_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + # don't reuse single variable to avoid bug #: + # http://projects.puppetlabs.com/issues/22455 + # TODO: facter should support native list types :) + result[x].join(',') + } + end +end + +# list of generated gluster_ports_volume's +Facter.add('gluster_brick_group_facts') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + result.keys.collect {|x| 'gluster_brick_group_'+x }.join(',') + } +end + +# vim: ts=8 diff --git a/gluster/lib/facter/gluster_fsm.rb b/gluster/lib/facter/gluster_fsm.rb new file mode 100644 index 000000000..fae84cca9 --- /dev/null +++ b/gluster/lib/facter/gluster_fsm.rb @@ -0,0 +1,162 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' +require 'base64' # TODO: i wish facter worked with types instead of hacks + +# regexp to match gluster volume name, eg: testvolume +volume_regexp = /^[a-z]{1}[a-z0-9]{0,}$/ # TODO: is this perfect ? + +# returns true if each brick in the list matches +def brick_match(l) + # regexp to match a brick pattern, eg: annex1.example.com:/storage1a + brick_regexp = /^[a-zA-Z]{1}[a-zA-Z0-9\.\-]{0,}:\/[a-zA-Z0-9]{1}[a-zA-Z0-9\/\.\-]{0,}$/ # TODO: is this perfect ? + l.each do |x| + if not brick_regexp.match(x) + return false + end + end + return true +end + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't collect... + fsm_dir = nil +else + module_vardir = var+'gluster/' + valid_dir = module_vardir.gsub(/\/$/, '')+'/' + fsm_dir = valid_dir+'volume/fsm/' +end + +state = {} +stack = {} +watch = {} + +if not(fsm_dir.nil?) and File.directory?(fsm_dir) + # loop through each sub directory in the gluster::volume fsm + Dir.glob(fsm_dir+'*').each do |d| + n = File.basename(d) # should be the gluster::volume name + if n.length > 0 and volume_regexp.match(n) + + f = d.gsub(/\/$/, '')+'/state' # full file path + if File.exists?(f) + # TODO: future versions should unpickle (but with yaml) + v = File.open(f, 'r').read.strip # read into str + if v.length > 0 and brick_match(v.split(',')) + state[n] = v + end + end + + f = d.gsub(/\/$/, '')+'/stack' # full file path + if File.exists?(f) + stack[n] = [] # initialize empty array + File.readlines(f).each do |l| + l = l.strip # clean off /n's + # TODO: future versions should unpickle (but with yaml) + if l.length > 0 and brick_match(l.split(',')) + #stack[n].push(l) + stack[n].push(Base64.encode64(l).delete("\n")) + end + end + end + + f = d.gsub(/\/$/, '')+'/watch' # full file path + if File.exists?(f) + watch[n] = [] # initialize empty array + File.readlines(f).each do |l| + l = l.strip # clean off /n's + # TODO: future versions should unpickle (but with yaml) + if l.length > 0 and brick_match(l.split(',')) + #watch[n].push(l) + watch[n].push(Base64.encode64(l).delete("\n")) + end + end + end + + end + end +end + +state.keys.each do |x| + Facter.add('gluster_volume_fsm_state_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + state[x] + } + end + + if stack.key?(x) + Facter.add('gluster_volume_fsm_stack_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + stack[x].join(',') + } + end + end + + if watch.key?(x) + Facter.add('gluster_volume_fsm_watch_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + watch[x].join(',') + } + end + end +end + +# list of gluster volume fsm state fact names +Facter.add('gluster_volume_fsm_states') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + state.keys.sort.collect {|x| 'gluster_volume_fsm_state_'+x }.join(',') + } +end + +Facter.add('gluster_volume_fsm_stacks') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + (state.keys & stack.keys).sort.collect {|x| 'gluster_volume_fsm_stack_'+x }.join(',') + } +end + +Facter.add('gluster_volume_fsm_watchs') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + (state.keys & watch.keys).sort.collect {|x| 'gluster_volume_fsm_watch_'+x }.join(',') + } +end + +Facter.add('gluster_fsm_debug') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + 'Oh cool, james added fsm support to puppet-gluster. Sweet!' + } +end + diff --git a/gluster/lib/facter/gluster_fsuuid.rb b/gluster/lib/facter/gluster_fsuuid.rb new file mode 100644 index 000000000..1f340750c --- /dev/null +++ b/gluster/lib/facter/gluster_fsuuid.rb @@ -0,0 +1,148 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' +require 'digest/sha1' + +# TODO: the ruby uuid method can be used when newer ruby versions are used here +# require 'securerandom' +# SecureRandom.uuid + +# uuid regexp +regexp = /^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$/ +fqdn = Facter.value('fqdn') # this could be nil ! + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't continue + module_vardir = nil + valid_brickdir = nil + uuiddir = nil +else + module_vardir = var+'gluster/' + valid_brickdir = module_vardir.gsub(/\/$/, '')+'/brick/' + uuiddir = valid_brickdir+'fsuuid/' # safe dir that won't get purged... +end + +# NOTE: module specific mkdirs, needed to ensure there is no blocking/deadlock! +if not(var.nil?) and not File.directory?(var) + Dir::mkdir(var) +end + +if not(module_vardir.nil?) and not File.directory?(module_vardir) + Dir::mkdir(module_vardir) +end + +if not(valid_brickdir.nil?) and not File.directory?(valid_brickdir) + Dir::mkdir(valid_brickdir) +end + +found = {} + +# generate uuid and parent directory if they don't already exist... +if not(valid_brickdir.nil?) and File.directory?(valid_brickdir) + if not File.directory?(uuiddir) + Dir::mkdir(uuiddir) + end + + # loop through brick dir, looking for brick names to make fsuuid's for! + if not(valid_brickdir.nil?) and File.directory?(valid_brickdir) and File.directory?(uuiddir) + Dir.glob(valid_brickdir+'*.*').each do |f| + b = File.basename(f) + g = b.split('.') # $name.group + + group = g.pop() # pop off suffix (the group name) + + if g.length >= 1 + # NOTE: some of this code is unnecessary, but i + # kept it because it matches the brick parsing. + + x = g.join('.') # in case value had dots in it. + + brick = File.open(f, 'r').read.strip # read into str + # eg: annex1.example.com:/storage1a + split = brick.split(':') # do some $name parsing + host = split[0] # host fqdn + # NOTE: technically $path should be everything BUT split[0]. This + # lets our $path include colons if for some reason they're needed. + #path = split[1] # brick mount or storage path + path = brick.slice(host.length+1, brick.length-host.length-1) + + # if fqdn is nil, generate for everyone... + # (other hosts data will just be unused...) + if not(fqdn.nil?) + # otherwise, skip hosts that aren't us! + if host != fqdn + next + end + end + + uuidfile = uuiddir + b + # we sha1 to prevent weird characters in facter + key = Digest::SHA1.hexdigest(host + ':' + path + '.' + group) + + # create an fsuuid for each brick and store it + # in our vardir if it doesn't already exist... + if not File.exist?(uuidfile) + result = system("/usr/bin/uuidgen > '" + uuidfile + "'") + if not(result) + # TODO: print warning + end + end + + # create facts from all the uuid files found... + uuid = File.open(uuidfile, 'r').read.strip.downcase # read into str + if uuid.length == 36 and regexp.match(uuid) + # avoid: http://projects.puppetlabs.com/issues/22455 + found[key] = uuid + # TODO: print warning on else... + end + end + end + end +end + +found.keys.each do |x| + Facter.add('gluster_brick_fsuuid_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found[x] + } + end +end + +# list of generated gluster_brick_fsuuid's +Facter.add('gluster_brick_fsuuid_facts') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found.keys.collect {|x| 'gluster_brick_fsuuid_'+x }.join(',') + } +end + +# vim: ts=8 diff --git a/gluster/lib/facter/gluster_host.rb b/gluster/lib/facter/gluster_host.rb new file mode 100644 index 000000000..181abb833 --- /dev/null +++ b/gluster/lib/facter/gluster_host.rb @@ -0,0 +1,35 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' +require 'resolv' + +# try and pick the _right_ ip that gluster should use by default... +fqdn = Facter.value('fqdn') +if not fqdn.nil? + ip = Resolv.getaddress "#{fqdn}" + if not ip.nil? + Facter.add('gluster_host_ip') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + ip + } + end + end +end + +# vim: ts=8 diff --git a/gluster/lib/facter/gluster_ports.rb b/gluster/lib/facter/gluster_ports.rb new file mode 100644 index 000000000..209538a57 --- /dev/null +++ b/gluster/lib/facter/gluster_ports.rb @@ -0,0 +1,89 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' + +# get the gluster path. this fact comes from an external fact set in: params.pp +gluster = Facter.value('gluster_program_gluster').to_s.chomp +if gluster == '' + gluster = `which gluster 2> /dev/null`.chomp + if gluster == '' + gluster = '/usr/sbin/gluster' + end +end + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't continue + xmlfile = nil +else + module_vardir = var+'gluster/' + xmlfile = module_vardir+'xml.py' +end + +host = Facter.value('fqdn') +found = {} + +# we need the script installed first to be able to generate the port facts... +if not(xmlfile.nil?) and File.exist?(xmlfile) + volumes = `#{gluster} volume list` + if $?.exitstatus == 0 + volumes.split.each do |x| + # values come out as comma separated strings for direct usage + cmd = gluster+' volume status --xml | '+xmlfile+" ports --volume '"+x+"' --host '"+host+"'" + result = `#{cmd}` + if $?.exitstatus == 0 + found[x] = result + # TODO: else, print warning + end + end + # TODO: else, print warning + end +end + +found.keys.each do |x| + Facter.add('gluster_ports_volume_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + # don't reuse single variable to avoid bug #: + # http://projects.puppetlabs.com/issues/22455 + found[x] + } + end +end + +# list of generated gluster_ports_volume's +Facter.add('gluster_ports_volumes_facts') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found.keys.collect {|x| 'gluster_ports_volume_'+x }.join(',') + } +end + +# vim: ts=8 diff --git a/gluster/lib/facter/gluster_property.rb b/gluster/lib/facter/gluster_property.rb new file mode 100644 index 000000000..d38614e1d --- /dev/null +++ b/gluster/lib/facter/gluster_property.rb @@ -0,0 +1,103 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' + +groupdir = '/var/lib/glusterd/groups/' # dir from the upstream package + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't continue + valid_setgroupdir = nil +else + module_vardir = var+'gluster/' + valid_setgroupdir = module_vardir.gsub(/\/$/, '')+'/groups/' +end + +found = {} + +# loop through each directory to avoid code duplication... later dirs override! +[valid_setgroupdir, groupdir].each do |g| + + if not(g.nil?) and File.directory?(g) + Dir.glob(g+'*').each do |f| + b = File.basename(f) + + # a later entry overrides an earlier one... + #if not found.key?(b) + found[b] = {} # initialize (or erase) + #end + + groups = File.open(f, 'r').read # read into str + groups.each_line do |line| + split = line.strip.split('=') # split key=value pairs + if split.length == 2 + key = split[0] + value = split[1] + if found[b].key?(key) + # NOTE: error found in file... + print "There is a duplicate key in the '#{b}' group." + end + found[b][key] = value + end + end + end + end +end + +# list of available property groups +Facter.add('gluster_property_groups') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found.keys.sort.join(',') + } +end + +# each group's list of key value pairs +found.keys.each do |x| + Facter.add('gluster_property_group_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + # don't reuse single variable to avoid bug #: + # http://projects.puppetlabs.com/issues/22455 + # TODO: facter should support native hash types :) + found[x].collect{|k,v| k+'='+v}.join(',') + } + end +end + +# has the custom group directory been created yet? +Facter.add('gluster_property_groups_ready') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + (File.directory?(valid_setgroupdir) ? 'true':'false') + } +end + +# vim: ts=8 diff --git a/gluster/lib/facter/gluster_uuid.rb b/gluster/lib/facter/gluster_uuid.rb new file mode 100644 index 000000000..fa99a4026 --- /dev/null +++ b/gluster/lib/facter/gluster_uuid.rb @@ -0,0 +1,153 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' + +# TODO: the ruby uuid method can be used when newer ruby versions are used here +# require 'securerandom' +# SecureRandom.uuid + +# uuid regexp +regexp = /^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$/ + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't continue + module_vardir = nil + uuiddir = nil + uuidfile = nil +else + module_vardir = var+'gluster/' + uuiddir = module_vardir+'uuid/' # safe dir that won't get purged... + uuidfile = uuiddir+'uuid' +end + +# NOTE: module specific mkdirs, needed to ensure there is no blocking/deadlock! +if not(var.nil?) and not File.directory?(var) + Dir::mkdir(var) +end + +if not(module_vardir.nil?) and not File.directory?(module_vardir) + Dir::mkdir(module_vardir) +end + +if not(uuiddir.nil?) and not File.directory?(uuiddir) + Dir::mkdir(uuiddir) +end + +# generate uuid and parent directory if they don't already exist... +if not(module_vardir.nil?) and File.directory?(module_vardir) + + create = false + if File.directory?(uuiddir) + + if File.exist?(uuidfile) + test = File.open(uuidfile, 'r').read.strip.downcase # read into str + # skip over uuid's of the wrong length or that don't match (security!!) + if test.length == 36 and regexp.match(test) + create = false + else + create = true + end + else + create = true + end + end + + # create a uuid and store it in our vardir if it doesn't already exist! + if create + result = system("/usr/bin/uuidgen > '" + uuidfile + "'") + if not(result) + # TODO: print warning + end + end +end + +# create the fact if the uuid file contains a valid uuid +if not(uuidfile.nil?) and File.exist?(uuidfile) + uuid = File.open(uuidfile, 'r').read.strip.downcase # read into str + # skip over uuid's of the wrong length or that don't match (security!!) + if uuid.length == 36 and regexp.match(uuid) + Facter.add('gluster_uuid') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + # don't reuse uuid variable to avoid bug #: + # http://projects.puppetlabs.com/issues/22455 + uuid + } + end + # TODO: print warning on else... + end +end + +# create facts from externally collected uuid files +_uuid = '' +found = {} +prefix = 'uuid_' +if not(uuiddir.nil?) and File.directory?(uuiddir) + Dir.glob(uuiddir+prefix+'*').each do |f| + + b = File.basename(f) + # strip off leading prefix + fqdn = b[prefix.length, b.length-prefix.length] + + _uuid = File.open(f, 'r').read.strip.downcase # read into str + if _uuid.length == 36 and regexp.match(_uuid) + # avoid: http://projects.puppetlabs.com/issues/22455 + found[fqdn] = _uuid + # TODO: print warning on else... + end + end +end + +found.keys.each do |x| + Facter.add('gluster_uuid_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found[x] + } + end +end + +# list of generated gluster_uuid's +Facter.add('gluster_uuid_facts') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found.keys.collect {|x| 'gluster_uuid_'+x }.join(',') + } +end + +Facter.add('gluster_fqdns') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found.keys.sort.join(',') + } +end + +# vim: ts=8 diff --git a/gluster/lib/facter/gluster_version.rb b/gluster/lib/facter/gluster_version.rb new file mode 100644 index 000000000..a3df7bc79 --- /dev/null +++ b/gluster/lib/facter/gluster_version.rb @@ -0,0 +1,40 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' + +# get the gluster path. this fact comes from an external fact set in: params.pp +gluster = Facter.value('gluster_program_gluster').to_s.chomp +if gluster == '' + gluster = `which gluster 2> /dev/null`.chomp + if gluster == '' + gluster = '/usr/sbin/gluster' + end +end +cut = `which cut 2> /dev/null`.chomp + +# create the fact if the gluster executable exists +if File.exist?(gluster) + Facter.add('gluster_version') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + Facter::Util::Resolution.exec(gluster+' --version | /usr/bin/head -1 | '+cut+' -d " " -f 2').chomp + } + end +end + +# vim: ts=8 diff --git a/gluster/lib/facter/gluster_vrrp.rb b/gluster/lib/facter/gluster_vrrp.rb new file mode 100644 index 000000000..79676e671 --- /dev/null +++ b/gluster/lib/facter/gluster_vrrp.rb @@ -0,0 +1,201 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' +require 'digest/sha1' +require 'ipaddr' + +length = 16 +# pass regexp +regexp = /^[a-zA-Z0-9]{#{length}}$/ +ipregexp = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ +netmaskregexp = /^(((128|192|224|240|248|252|254)\.0\.0\.0)|(255\.(0|128|192|224|240|248|252|254)\.0\.0)|(255\.255\.(0|128|192|224|240|248|252|254)\.0)|(255\.255\.255\.(0|128|192|224|240|248|252|254)))$/ +chars = [('a'..'z'), ('A'..'Z'), (0..9)].map { |i| i.to_a }.flatten + + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't continue + module_vardir = nil + vrrpdir = nil + vrrpfile = nil + ipfile = nil +else + module_vardir = var+'gluster/' + vrrpdir = module_vardir+'vrrp/' + vrrpfile = vrrpdir+'vrrp' + ipfile = vrrpdir+'ip' +end + +# NOTE: module specific mkdirs, needed to ensure there is no blocking/deadlock! +if not(var.nil?) and not File.directory?(var) + Dir::mkdir(var) +end + +if not(module_vardir.nil?) and not File.directory?(module_vardir) + Dir::mkdir(module_vardir) +end + +if not(vrrpdir.nil?) and not File.directory?(vrrpdir) + Dir::mkdir(vrrpdir) +end + +# generate pass and parent directory if they don't already exist... +if not(module_vardir.nil?) and File.directory?(module_vardir) + if not File.directory?(vrrpdir) + Dir::mkdir(vrrpdir) + end + + # create a pass and store it in our vardir if it doesn't already exist! + if File.directory?(vrrpdir) and ((not File.exist?(vrrpfile)) or (File.size(vrrpfile) == 0)) + # include a built-in pwgen-like backup + string = (0..length-1).map { chars[rand(chars.length)] }.join + result = system("(/usr/bin/test -z /usr/bin/pwgen && /usr/bin/pwgen -N 1 #{length} || /bin/echo '#{string}') > '" + vrrpfile + "'") + if not(result) + # TODO: print warning + end + end +end + +# create the fact if the vrrp file contains a valid pass +if not(vrrpfile.nil?) and File.exist?(vrrpfile) + pass = File.open(vrrpfile, 'r').read.strip # read into str + # skip over pass's of the wrong length or that don't match (security!!) + if pass.length == length and regexp.match(pass) + Facter.add('gluster_vrrp') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + # don't reuse pass variable to avoid bug #: + # http://projects.puppetlabs.com/issues/22455 + pass + } + end + # TODO: print warning on else... + end +end + +# create facts from externally collected vrrp files +_pass = '' +found = {} +prefix = 'vrrp_' +if not(vrrpdir.nil?) and File.directory?(vrrpdir) + Dir.glob(vrrpdir+prefix+'*').each do |f| + + b = File.basename(f) + # strip off leading prefix + fqdn = b[prefix.length, b.length-prefix.length] + + _pass = File.open(f, 'r').read.strip.downcase # read into str + if _pass.length == length and regexp.match(_pass) + # avoid: http://projects.puppetlabs.com/issues/22455 + found[fqdn] = _pass + # TODO: print warning on else... + end + end +end + +#found.keys.each do |x| +# Facter.add('gluster_vrrp_'+x) do +# #confine :operatingsystem => %w{CentOS, RedHat, Fedora} +# setcode { +# found[x] +# } +# end +#end + +#Facter.add('gluster_vrrp_facts') do +# #confine :operatingsystem => %w{CentOS, RedHat, Fedora} +# setcode { +# found.keys.collect {|x| 'gluster_vrrp_'+x }.join(',') +# } +#end + +# distributed password (uses a piece from each host) +collected = found.keys.sort.collect {|x| found[x] }.join('#') # combine pieces +Facter.add('gluster_vrrp_password') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + Digest::SHA1.hexdigest(collected) + } +end + +Facter.add('gluster_vrrp_fqdns') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + # sorting is very important + found.keys.sort.join(',') + } +end + +# create these facts if the ip file contains a valid ip address +if not(ipfile.nil?) and File.exist?(ipfile) + ip = File.open(ipfile, 'r').read.strip.downcase # read into str + # skip over ip that doesn't match (security!!) + if ipregexp.match(ip) + + # TODO: replace with system-getifaddrs if i can get it working! + cmd = "/sbin/ip -o a show to #{ip} | /bin/awk '{print $2}'" + interface = `#{cmd}`.strip + if $?.exitstatus == 0 and interface.length > 0 + + Facter.add('gluster_vrrp_interface') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + interface + } + end + + # lookup from fact + netmask = Facter.value('netmask_'+interface) + if netmaskregexp.match(netmask) + + Facter.add('gluster_vrrp_netmask') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + netmask + } + end + + cidr = IPAddr.new("#{netmask}").to_i.to_s(2).count('1') + Facter.add('gluster_vrrp_cidr') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + cidr + } + end + end + + # TODO: print warning on else... + end + + # TODO: print warning on else... + end +end + +# vim: ts=8 diff --git a/gluster/lib/puppet/parser/functions/brick_layout_chained.rb b/gluster/lib/puppet/parser/functions/brick_layout_chained.rb new file mode 100644 index 000000000..0dff9aa7c --- /dev/null +++ b/gluster/lib/puppet/parser/functions/brick_layout_chained.rb @@ -0,0 +1,137 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +module Puppet::Parser::Functions + newfunction(:brick_layout_chained, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Return the complex chained brick list + + Example: + + $layout = brick_layout_chained($replica, $bricks) + notice("layout is: ${layout}") + + This function is used internally for automatic brick layouts. + + ENDHEREDOC + + Puppet::Parser::Functions.function('warning') # load function + # signature: replica, bricks -> bricks + unless args.length == 2 + raise Puppet::ParseError, "brick_layout_chained(): wrong number of arguments (#{args.length}; must be 2)" + end + if not(args[0].is_a?(Integer)) and not(args[0].is_a?(String)) + # NOTE: strings that convert to int's with .to_i are ok + raise Puppet::ParseError, "brick_layout_chained(): expects the first argument to be an integer, got #{args[0].inspect} which is of type #{args[0].class}" + end + unless args[1].is_a?(Array) + raise Puppet::ParseError, "brick_layout_chained(): expects the first argument to be an array, got #{args[1].inspect} which is of type #{args[1].class}" + end + + replica = args[0].to_i # convert from string if needed + bricks = args[1] + + # TODO: these functions could be in separate puppet files + # eg: Puppet::Parser::Functions.function('myfunc') + # function_myfunc(...) + def brick_str_to_hash(bricks) + # this loop converts brick strings to brick dict's... + result = [] + bricks.each do |x| + a = x.split(':') + #assert a.length == 2 # TODO + p = a[1] + p = ((p[-1, 1] == '/') ? p : (p+'/')) # endswith + + result.push({'host'=> a[0], 'path'=> p}) + end + return result + end + + def get_hostlist(bricks) + hosts = [] + bricks.each do |x| + key = x['host'] + val = x['path'] + + if not hosts.include?(key) + hosts.push(key) + end + end + return hosts + end + + def get_brickstacks(bricks, sort=false) + stacks = {} + hosts = get_hostlist(bricks) + bricks.each do |x| + key = x['host'] + val = x['path'] + if not stacks.include?(key); stacks[key] = []; end # initialize + stacks[key].push(val) + end + + # optionally sort the paths in each individual host stack... + if sort + sorted_stacks = {} + stacks.each do |k, v| + # TODO: there should probably be a proper 'sorted' function for + # paths, in case they aren't numbered sanely _WITH_ padding. + sorted_stacks[k] = v.sort + end + return sorted_stacks + end + return stacks + end + + final = [] + pointer = 0 + parsed = brick_str_to_hash(bricks) + # TODO: there should probably be a proper 'sorted' function for + # hostnames, in case they aren't numbered sanely _WITH_ padding. + hosts = get_hostlist(parsed).sort + brickstack = get_brickstacks(parsed, sort=true) + + if bricks.length == 0; return []; end + + # FIXME: this works with homogeneous volumes only! + while pointer < (hosts.length * brickstack[hosts[0]].length) do + start = hosts[pointer % hosts.length] + #puts "host is #{host}, pointer is: #{pointer}" + + index = 0 + while index < replica do + host = hosts[(pointer+index) % hosts.length] + #puts "host is #{host}, index is: #{index}" + index+= 1 + + path = brickstack[host].shift # yoink + if path.nil? + function_warning(["brick_layout_chained(): brick list is not valid"]) + next + end + final.push({'host' => host, 'path' => path}) # save + end + pointer+=1 + end + + # build final result + result = final.collect {|x| x['host']+':'+x['path'] } + result # return + end +end + +# vim: ts=8 diff --git a/gluster/lib/puppet/parser/functions/brick_layout_simple.rb b/gluster/lib/puppet/parser/functions/brick_layout_simple.rb new file mode 100644 index 000000000..aefc1e29e --- /dev/null +++ b/gluster/lib/puppet/parser/functions/brick_layout_simple.rb @@ -0,0 +1,111 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: +# sort the bricks in a logical manner... i think this is the optimal algorithm, +# but i'd be happy if someone thinks they can do better! this assumes that the +# bricks and hosts are named in a logical manner. alphanumeric sorting is used +# to determine the default ordering... + +module Puppet::Parser::Functions + newfunction(:brick_layout_simple, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Return the simple symmetrical brick list + + Example: + + $layout = brick_layout_simple($replica, $bricks) + notice("layout is: ${layout}") + + This function is used internally for automatic brick layouts. + + ENDHEREDOC + + # signature: replica, bricks -> bricks + unless args.length == 2 + raise Puppet::ParseError, "brick_layout_simple(): wrong number of arguments (#{args.length}; must be 2)" + end + if not(args[0].is_a?(Integer)) and not(args[0].is_a?(String)) + # NOTE: strings that convert to int's with .to_i are ok + raise Puppet::ParseError, "brick_layout_simple(): expects the first argument to be an integer, got #{args[0].inspect} which is of type #{args[0].class}" + end + unless args[1].is_a?(Array) + raise Puppet::ParseError, "brick_layout_simple(): expects the first argument to be an array, got #{args[1].inspect} which is of type #{args[1].class}" + end + + replica = args[0].to_i # convert from string if needed + bricks = args[1] + + # TODO: these functions could be in separate puppet files + # eg: Puppet::Parser::Functions.function('myfunc') + # function_myfunc(...) + def brick_str_to_hash(bricks) + # this loop converts brick strings to brick dict's... + result = [] + bricks.each do |x| + a = x.split(':') + #assert a.length == 2 # TODO + p = a[1] + p = ((p[-1, 1] == '/') ? p : (p+'/')) # endswith + + result.push({'host'=> a[0], 'path'=> p}) + end + return result + end + + collect = {} + parsed = brick_str_to_hash(bricks) + parsed.each do |x| + key = x['host'] + val = x['path'] + + if not collect.has_key?(key) + collect[key] = [] # initialize + end + + collect[key].push(val) # save in array + # TODO: ensure this array is always sorted (we could also do this after + # or always insert elements in the correct sorted order too :P) + collect[key] = collect[key].sort + end + + # we also could do this sort here... + collect.keys.each do |x| + collect[x] = collect[x].sort + end + + final = [] # final order... + # TODO: here we can probably detect if this is an asymmetrical configurations, or maybe bad naming... + while collect.size > 0 + collect.keys.sort.each do |x| + + # NOTE: this array should already be sorted! + p = collect[x].shift # assume an array of at least 1 element + final.push( { 'host' => x, 'path' => p } ) # save + + if collect[x].size == 0 # maybe the array is empty now + collect.delete(x) # remove that empty list's key + end + end + end + + # build final result + result = final.collect {|x| x['host']+':'+x['path'] } + result # return + end +end + +# vim: ts=8 diff --git a/gluster/manifests/again.pp b/gluster/manifests/again.pp new file mode 100644 index 000000000..63ee8d1be --- /dev/null +++ b/gluster/manifests/again.pp @@ -0,0 +1,39 @@ +# GlusterFS module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: use with: +# notify => Common::Again::Delta['gluster-exec-again'], + +# NOTE: this should be attached to the logical (often last) thing that, when it +# runs, means or is a signal that something more is going to happen in the next +# puppet run, and that stuff that is going to happen is useful to what just did +# run, which is why we don't want to wait for it to happen naturally in 30 min. + +class gluster::again { + + # TODO: we could include an option to disable this exec again and + # replace it with a "dummy" noop if someone doesn't want to use it. + include common::again + + # when notified, this will run puppet again, delta sec after it ends! + common::again::delta { 'gluster-exec-again': + delta => 120, # 2 minutes + } + +} + +# vim: ts=8 diff --git a/gluster/manifests/api.pp b/gluster/manifests/api.pp new file mode 100644 index 000000000..3f73baa2d --- /dev/null +++ b/gluster/manifests/api.pp @@ -0,0 +1,44 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::api( + $repo = true, # are we using the automatic repo ? + $version = '' # pick a specific version (defaults to latest) +) { + include gluster::params + + $rname = "${version}" ? { + '' => 'gluster', + default => "gluster-${version}", + } + + # certain packages don't exist on certain operating systems + if "${::gluster::params::package_glusterfs_api}" != '' { + package { "${::gluster::params::package_glusterfs_api}": + ensure => "${version}" ? { + '' => present, + default => "${version}", + }, + require => $repo ? { + false => undef, + default => Gluster::Repo["${rname}"], + }, + } + } +} + +# vim: ts=8 diff --git a/gluster/manifests/brick.pp b/gluster/manifests/brick.pp new file mode 100644 index 000000000..8358ad216 --- /dev/null +++ b/gluster/manifests/brick.pp @@ -0,0 +1,536 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +define gluster::brick( + $group = 'default', # grouping for multiple puppet-glusters + # if dev is false, path in $name is used directly after a mkdir -p + $dev = false, # /dev/sdc, /dev/disk/by-id/scsi-36003048007e14f0014ca2743150a5471 + + $raid_su = '', # used by mkfs.xfs and lvm, eg: 256 (K) + $raid_sw = '', # used by mkfs.xfs and lvm, eg: 10 + + $partition = true, # partition, or build on the block dev? + $labeltype = '', # gpt + + $lvm = true, # use lvm or not ? + $lvm_thinp = false, # use lvm thin-p or not ? + $lvm_virtsize = '', # defaults to 100% available. + $lvm_chunksize = '', # chunk size for thin-p + $lvm_metadatasize = '', # meta data size for thin-p + + $fsuuid = '', # set a uuid for this fs (uuidgen) + $fstype = '', # xfs + $ro = false, # use for emergencies only- you want your fs rw + + $xfs_inode64 = false, + $xfs_nobarrier = false, + $force = false, # if true, this will overwrite any xfs fs it sees, useful for rebuilding gluster and wiping data. NOTE: there are other safeties in place to stop this. + $areyousure = false, # do you allow puppet to do dangerous things ? + $again = true, # do we want to use Exec['again'] ? + $comment = '' +) { + include gluster::brick::base + if $again { + include gluster::again + } + include gluster::vardir + include gluster::params + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + # eg: annex1.example.com:/storage1a + $split = split($name, ':') # do some $name parsing + $host = $split[0] # host fqdn + # NOTE: technically $path should be everything BUT split[0]. This + # lets our $path include colons if for some reason they're needed. + #$path = $split[1] # brick mount or storage path + # TODO: create substring function + $path = inline_template("<%= '${name}'.slice('${host}'.length+1, '${name}'.length-'${host}'.length-1) %>") + $short_path = sprintf("%s", regsubst($path, '\/$', '')) # no trailing + $valid_path = sprintf("%s/", regsubst($path, '\/$', '')) + + if ! ( "${host}:${path}" == "${name}" ) { + fail('The brick $name must match a $host-$path pattern.') + } + + Gluster::Host[$host] -> Gluster::Brick[$name] # brick requires host + + # create a brick tag to be collected by the gluster_brick_group_* fact! + $safename = regsubst("${name}", '/', '_', 'G') # make /'s safe + file { "${vardir}/brick/${safename}.${group}": + content => "${name}\n", + owner => root, + group => root, + mode => 644, + ensure => present, + require => File["${vardir}/brick/"], + } + + # + # fsuuid... + # + if ("${fsuuid}" != '') and "${fsuuid}" =~ /^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$/ { + fail("The chosen fs uuid: '${fsuuid}' is not valid.") + } + + # if we manually *pick* a uuid, then store it too, so that it + # sticks if we ever go back to using automatic uuids. this is + # useful if a user wants to initially import uuids by picking + # them manually, and then letting puppet take over afterwards + if "${fsuuid}" != '' { + # $group is unnecessary, but i left it in for consistency... + file { "${vardir}/brick/fsuuid/${safename}.${group}": + content => "${fsuuid}\n", + owner => root, + group => root, + mode => 600, # might as well... + ensure => present, + require => File["${vardir}/brick/fsuuid/"], + } + } + + # we sha1 to prevent weird characters in facter + $fsuuid_safename = sha1("${name}.${group}") + $valid_fsuuid = "${fsuuid}" ? { + # fact from the data generated in: ${vardir}/brick/fsuuid/* + '' => getvar("gluster_brick_fsuuid_${fsuuid_safename}"), # fact! + default => "${fsuuid}", + } + + # you might see this on first run if the fsuuid isn't generated yet + if (type($dev) != 'boolean') and ("${valid_fsuuid}" == '') { + warning('An $fsuuid must be specified or generated.') + } + + # + # raid... + # + # TODO: check inputs for sanity and auto-detect if one is empty + # TODO: maybe we can detect these altogether from the raid set! + if "${raid_su}" == '' and "${raid_sw}" == '' { + # if we are not using a real device, we should ignore warnings! + if type($dev) != 'boolean' { # real devices! + if $lvm or "${fstype}" == 'xfs' { + warning('Setting $raid_su and $raid_sw is recommended.') + } + } + } elsif "${raid_su}" != '' and "${raid_sw}" != '' { + # ensure both are positive int's ! + validate_re("${raid_su}", '^\d+$') + validate_re("${raid_sw}", '^\d+$') + + } else { + fail('You must set both $raid_su and $raid_sw or neither.') + } + + # + # partitioning... + # + $valid_labeltype = $labeltype ? { + #'msdos' => 'msdos', # TODO + default => 'gpt', + } + + # get the raw /dev/vdx device, and append the partition number + $dev0 = "`/bin/readlink -e ${dev}`" # resolve to /dev/ + + $part_mklabel = "${::gluster::params::program_parted} -s -m -a optimal ${dev0} mklabel ${valid_labeltype}" + $part_mkpart = "${::gluster::params::program_parted} -s -m -a optimal ${dev0} mkpart primary 0% 100%" + + # + $dev1 = $partition ? { + false => "${dev0}", # block device without partition + default => "${dev0}1", # partition one (eg: /dev/sda1) + } + + # + # lvm... + # + if $lvm_thinp and ( ! $lvm ) { + warning('You must enable $lvm if you want to use LVM thin-p.') + } + + if $lvm { + # NOTE: this is used for thin-provisioning, and RHS compliance! + + # NOTE: as a consequence of this type of automation, we generate + # really ugly vg names like: "vg_annex1.example.com+_gluster_" ! + # TODO: in the future, it might be nice to provide an option to + # use simplified naming based on hostname and a brick number... + $lvm_safename = regsubst("${safename}", ':', '+', 'G') # safe! + $lvm_vgname = "vg_${lvm_safename}" + $lvm_lvname = "lv_${lvm_safename}" + $lvm_tpname = "tp_${lvm_safename}" # thin pool (tp) + + $lvm_dataalignment = inline_template('<%= @raid_su.to_i*@raid_sw.to_i %>') + + $lvm_pvcreate = "${raid_su}${raid_sw}" ? { # both empty ? + '' => "${::gluster::params::program_pvcreate} ${dev1}", + default => "${::gluster::params::program_pvcreate} --dataalignment ${lvm_dataalignment}K ${dev1}", + } + + $lvm_vgcreate = "${::gluster::params::program_vgcreate} ${lvm_vgname} ${dev1}" + + # match --virtualsize with 100% of available vg by default + $lvm_thinp_virtsize = "${lvm_virtsize}" ? { # --virtualsize + '' => "`${::gluster::params::program_vgs} -o size --units b --noheadings ${lvm_vgname}`", + default => "${lvm_virtsize}", + } + + # TODO: is 64k a good/sane default ? + $lvm_thinp_chunksize = "${lvm_chunksize}" ? { + '' => '', + default => "--chunksize ${lvm_chunksize}", + } + + # TODO: is 16384 a good/sane default ? + $lvm_thinp_metadatasize = "${lvm_metadatasize}" ? { + '' => '', + default => "--poolmetadatasize ${lvm_metadatasize}", + } + + # README: 'man 7 lvmthin' to understand lvm thin provisioning + # MIRROR: http://man7.org/linux/man-pages/man7/lvmthin.7.html + # TODO: is this the optimal setup for thin-p ? + $lvm_thinp_lvcreate_cmdlist = [ + "${::gluster::params::program_lvcreate}", + "--thinpool ${lvm_vgname}/${lvm_tpname}", # thinp + '--extents 100%FREE', # let lvm figure out the --size + "--virtualsize ${lvm_thinp_virtsize}", + "${lvm_thinp_chunksize}", + "${lvm_thinp_metadatasize}", + " -n ${lvm_lvname}", # name it + ] + $lvm_thinp_lvcreate = join(delete($lvm_thinp_lvcreate_cmdlist, ''), ' ') + + # creates dev /dev/vgname/lvname + $lvm_lvcreate = $lvm_thinp ? { + true => "${lvm_thinp_lvcreate}", + default => "${::gluster::params::program_lvcreate} --extents 100%PVS -n ${lvm_lvname} ${lvm_vgname}", + } + } + + $dev2 = $lvm ? { + false => "${dev1}", # pass through, because not using lvm + default => "/dev/${lvm_vgname}/${lvm_lvname}", # thin-p too :) + } + + # + # mkfs... + # + $ro_bool = $ro ? { # this has been added as a convenience + true => 'ro', + default => 'rw', + } + + # if $dev is false, we assume we're using a path backing store on brick + $valid_fstype = type($dev) ? { + 'boolean' => $dev ? { + false => 'path', # no dev, just a path spec + default => '', # invalid type + }, + default => $fstype ? { + 'ext4' => 'ext4', # TODO + 'btrfs' => 'btrfs', + default => 'xfs', + }, + } + + if ( $valid_fstype == 'path' ) { + + # do a mkdir -p in the execution section below... + $options_list = [] # n/a + + # XFS mount options: + # http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=blob;f=Documentation/filesystems/xfs.txt;hb=HEAD + } elsif ( $valid_fstype == 'xfs' ) { + # exec requires + include gluster::brick::xfs + $exec_requires = [Package["${::gluster::params::package_xfsprogs}"]] + + $xfs_arg00 = "${::gluster::params::program_mkfs_xfs}" + + $xfs_arg01 = '-q' # shh! + + # NOTE: the -f forces creation when it sees an old xfs part + $xfs_arg02 = $force ? { + true => '-f', + default => '', + } + + # Due to extensive use of extended attributes, RHS recommends + # XFS inode size set to 512 bytes from the defaults 256 Bytes. + $xfs_arg03 = '-i size=512' + + # An XFS file system allows you to select a logical block size + # for the file-system directory that is greater than the + # logical block size of the file-system. Increasing the logical + # block size for the directories from the default of 4K, + # decreases the directory IO, which improves the performance of + # directory operations. See: + # http://xfs.org/index.php/XFS_FAQ#Q:_Performance:_mkfs.xfs_-n_size.3D64k_option + $xfs_arg04 = '-n size=8192' + + # To align the IO at the file system layer it is important that + # we set the correct stripe unit (stripe element size) and + # stripe width (number of data disks) while formatting the file + # system. These options are sometimes auto-detected but manual + # configuration is needed with many of the hardware RAID + # volumes. + $xfs_arg05 = "${raid_su}${raid_sw}" ? { # both empty ? + '' => '', + default => "-d su=${raid_su}k,sw=${raid_sw}", + } + + $xfs_cmdlist = [ + "${xfs_arg00}", + "${xfs_arg01}", + "${xfs_arg02}", + "${xfs_arg03}", + "${xfs_arg04}", + "${xfs_arg05}", + "${dev2}" + ] + $xfs_cmd = join(delete($xfs_cmdlist, ''), ' ') + + # TODO: xfs_admin doesn't have a --quiet flag. silence it... + $xfs_admin = "${::gluster::params::program_xfsadmin} -U '${valid_fsuuid}' ${dev2}" + + # mkfs w/ uuid command + $mkfs_exec = "${xfs_cmd} && ${xfs_admin}" + + # By default, XFS allocates inodes to reflect their on-disk + # location. However, because some 32-bit userspace applications + # are not compatible with inode numbers greater than 232, XFS + # will allocate all inodes in disk locations which result in + # 32-bit inode numbers. This can lead to decreased performance + # on very large filesystems (i.e. larger than 2 terabytes), + # because inodes are skewed to the beginning of the block + # device, while data is skewed towards the end. + # To address this, use the inode64 mount option. This option + # configures XFS to allocate inodes and data across the entire + # file system, which can improve performance. + $option01 = $xfs_inode64 ? { + true => 'inode64', + default => '', + } + + # By default, XFS uses write barriers to ensure file system + # integrity even when power is lost to a device with write + # caches enabled. For devices without write caches, or with + # battery-backed write caches, disable barriers using the + # nobarrier option. + $option02 = $xfs_nobarrier ? { + true => 'nobarrier', + default => '', + } + + $options_list = ["${option01}", "${option02}"] + + } elsif ( $valid_fstype == 'ext4' ) { + # exec requires + include gluster::brick::ext4 + $exec_requires = [Package["${::gluster::params::package_e2fsprogs}"]] + + # mkfs w/ uuid command + $mkfs_exec = "${::gluster::params::program_mkfs_ext4} -U '${valid_fsuuid}' ${dev2}" + + # mount options + $options_list = [] # TODO + + } elsif ( $valid_fstype == 'btrfs' ) { + # exec requires + include gluster::brick::btrfs + $exec_requires = [Package["${::gluster::params::package_btrfsprogs}"]] + + # FIXME: this filesystem has not yet been optimized for performance + + # mkfs w/ uuid command + $mkfs_exec = "${::gluster::params::program_mkfs_btrfs} -U '${valid_fsuuid}' ${dev2}" + + # mount options + $options_list = [] # TODO + + } else { + fail('The $fstype is invalid.') + } + + # put all the options in an array, remove the empty ones, and join with + # commas (this removes ',,' double comma uglyness) + # adding 'defaults' here ensures no ',' (leading comma) in mount command + $mount_options = inline_template('<%= (["defaults"]+@options_list).delete_if {|x| x.empty? }.join(",") %>') + + $exec_noop = $areyousure ? { + true => false, + default => true, + } + + # if we're on itself, and we have a real device to work with + if (type($dev) != 'boolean') and ("${fqdn}" == "${host}") { + + # partitioning... + if $partition { + if $exec_noop { + notify { "noop for partitioning: ${name}": + message => "${part_mklabel} && ${part_mkpart}", + } + } + + exec { "${part_mklabel} && ${part_mkpart}": + logoutput => on_failure, + unless => [ # if one element is true, this *doesn't* run + "/usr/bin/test -e ${dev1}", # does the partition 1 exist ? + '/bin/false', # TODO: add more criteria + ], + require => $exec_requires, + timeout => 3600, # TODO + noop => $exec_noop, + before => $lvm ? { # if no lvm, skip to mkfs + false => Exec["gluster-brick-mkfs-${name}"], + default => Exec["gluster-brick-lvm-pvcreate-${name}"], + }, + alias => "gluster-brick-partition-${name}", + } + } + + # lvm... + if $lvm { + if $exec_noop { + notify { "noop for lvm: ${name}": + message => "${lvm_pvcreate} && ${lvm_vgcreate} && ${lvm_lvcreate}", + } + } + + exec { "${lvm_pvcreate}": + logoutput => on_failure, + unless => [ # if one element is true, this *doesn't* run + "${::gluster::params::program_pvdisplay} ${dev1}", + '/bin/false', # TODO: add more criteria + ], + require => $exec_requires, + timeout => 3600, # set to something very long + noop => $exec_noop, + before => Exec["gluster-brick-lvm-vgcreate-${name}"], + alias => "gluster-brick-lvm-pvcreate-${name}", + } + + exec { "${lvm_vgcreate}": + logoutput => on_failure, + unless => [ # if one element is true, this *doesn't* run + "${::gluster::params::program_vgdisplay} ${lvm_vgname}", + '/bin/false', # TODO: add more criteria + ], + require => $exec_requires, + timeout => 3600, # set to something very long + noop => $exec_noop, + before => Exec["gluster-brick-lvm-lvcreate-${name}"], + alias => "gluster-brick-lvm-vgcreate-${name}", + } + + exec { "${lvm_lvcreate}": + logoutput => on_failure, + unless => [ # if one element is true, this *doesn't* run + #"${::gluster::params::program_lvdisplay} ${lvm_lvname}", # nope! + "${::gluster::params::program_lvs} --separator ':' | /usr/bin/tr -d ' ' | /bin/awk -F ':' '{print \$1}' | /bin/grep -q '${lvm_lvname}'", + '/bin/false', # TODO: add more criteria + ], + require => $exec_requires, + timeout => 3600, # set to something very long + noop => $exec_noop, + before => Exec["gluster-brick-mkfs-${name}"], + alias => "gluster-brick-lvm-lvcreate-${name}", + } + } + + if $exec_noop { + notify { "noop for mkfs: ${name}": + message => "${mkfs_exec}", + } + } else { + # if valid_fsuuid isn't ready, trigger an exec again... + exec { "gluster-brick-fsuuid-execagain-${name}": + command => '/bin/true', # do nothing but notify + logoutput => on_failure, + onlyif => "/usr/bin/test -z '${valid_fsuuid}'", + notify => $again ? { + false => undef, + default => Common::Again::Delta['gluster-exec-again'], + }, + # this (optional) require makes it more logical + require => File["${vardir}/brick/fsuuid/"], + } + } + + # mkfs... + exec { "${mkfs_exec}": + logoutput => on_failure, + onlyif => "/usr/bin/test -n '${valid_fsuuid}'", + unless => [ # if one element is true, this *doesn't* run + "/usr/bin/test -e /dev/disk/by-uuid/${valid_fsuuid}", + "${::gluster::params::program_findmnt} --output 'TARGET,SOURCE' -t ${valid_fstype} --target '${valid_path}' -n", + '/bin/false', # TODO: add more criteria + ], + require => $exec_requires, + timeout => 3600, # set to something very long + noop => $exec_noop, + alias => "gluster-brick-mkfs-${name}", + } + + # make an empty directory for the mount point + file { "${valid_path}": + ensure => directory, # make sure this is a directory + recurse => false, # don't recurse into directory + purge => false, # don't purge unmanaged files + force => false, # don't purge subdirs and links + require => Exec["gluster-brick-mkfs-${name}"], + } + + # mount points don't seem to like trailing slashes... + if "${valid_fsuuid}" != '' { # in case fsuuid isn't ready yet + mount { "${short_path}": + atboot => true, + ensure => mounted, + device => "UUID=${valid_fsuuid}", + fstype => "${valid_fstype}", + # noatime,nodiratime to save gluster from silly updates + options => "${mount_options},${ro_bool},noatime,nodiratime,noexec", # TODO: is nodev? nosuid? noexec? a good idea? + dump => '0', # fs_freq: 0 to skip file system dumps + # NOTE: technically this should be '2', to `fsck.xfs` + # after the rootfs ('1'), but fsck.xfs actually does + # 'nothing, successfully', so it's irrelevant, because + # xfs uses xfs_check and friends only when suspect. + pass => '2', # fs_passno: 0 to skip fsck on boot + require => [ + File["${valid_path}"], + ], + } + } + + } elsif ((type($dev) == 'boolean') and (! $dev)) and ("${fqdn}" == "${host}") { + + # ensure the full path exists! + exec { "/bin/mkdir -p '${valid_path}'": + creates => "${valid_path}", + logoutput => on_failure, + noop => $exec_noop, + alias => "gluster-brick-mkdir ${name}", + } + } +} + +# vim: ts=8 diff --git a/gluster/manifests/brick/base.pp b/gluster/manifests/brick/base.pp new file mode 100644 index 000000000..c39cb3f45 --- /dev/null +++ b/gluster/manifests/brick/base.pp @@ -0,0 +1,41 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::brick::base { + + include gluster::vardir + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + file { "${vardir}/brick/": + ensure => directory, # make sure this is a directory + recurse => true, # don't recurse into directory + purge => true, # don't purge unmanaged files + force => true, # don't purge subdirs and links + require => File["${vardir}/"], + } + + # don't purge the fsuuid file's generated within + file { "${vardir}/brick/fsuuid/": + ensure => directory, # make sure this is a directory + recurse => false, # don't recurse into directory + purge => false, # don't purge unmanaged files + force => false, # don't purge subdirs and links + require => File["${vardir}/brick/"], + } +} +# vim: ts=8 diff --git a/gluster/manifests/brick/btrfs.pp b/gluster/manifests/brick/btrfs.pp new file mode 100644 index 000000000..94232b380 --- /dev/null +++ b/gluster/manifests/brick/btrfs.pp @@ -0,0 +1,27 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::brick::btrfs { + + include gluster::params + + package { "${::gluster::params::package_btrfsprogs}": + ensure => present, + } +} + +# vim: ts=8 diff --git a/gluster/manifests/brick/ext4.pp b/gluster/manifests/brick/ext4.pp new file mode 100644 index 000000000..9cbef7407 --- /dev/null +++ b/gluster/manifests/brick/ext4.pp @@ -0,0 +1,27 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::brick::ext4 { + + include gluster::params + + package { "${::gluster::params::package_e2fsprogs}": + ensure => present, + } +} + +# vim: ts=8 diff --git a/gluster/manifests/brick/xfs.pp b/gluster/manifests/brick/xfs.pp new file mode 100644 index 000000000..8c1440cbe --- /dev/null +++ b/gluster/manifests/brick/xfs.pp @@ -0,0 +1,27 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::brick::xfs { + + include gluster::params + + package { "${::gluster::params::package_xfsprogs}": + ensure => present, + } +} + +# vim: ts=8 diff --git a/gluster/manifests/host.pp b/gluster/manifests/host.pp new file mode 100644 index 000000000..7f6efede0 --- /dev/null +++ b/gluster/manifests/host.pp @@ -0,0 +1,386 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# TODO: instead of peering manually this way (which makes the most sense, but +# might be unsupported by gluster) we could peer using the cli, and ensure that +# only the host holding the vip is allowed to execute cluster peer operations. + +define gluster::host( + $ip = '', # you can specify which ip address to use (if multiple) + $uuid = '', # if empty, puppet will attempt to use the gluster fact + $password = '' # if empty, puppet will attempt to choose one magically +) { + include gluster::vardir + include gluster::params + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + if ("${uuid}" != '') and (! ("${uuid}" =~ /^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$/)) { + fail("The chosen UUID: '${uuid}' is not valid.") + } + + Gluster::Host[$name] -> Service["${::gluster::params::service_glusterd}"] # glusterd requires host + + # if we're on itself + if "${fqdn}" == "${name}" { + + $valid_ip = "${ip}" ? { + '' => "${::gluster_host_ip}" ? { # smart fact... + '' => "${::ipaddress}", # puppet picks! + default => "${::gluster_host_ip}", # smart + }, + default => "${ip}", # user selected + } + if "${valid_ip}" == '' { + fail('No valid IP exists!') + } + + # store the ip here so that it can be accessed by bricks... + class { '::gluster::host::data': + #name => $name, + ip => "${valid_ip}", + fqdn => "${fqdn}", + } + + # don't purge the uuid file generated within + file { "${vardir}/uuid/": + ensure => directory, # make sure this is a directory + recurse => true, # recurse into directory + purge => true, # purge unmanaged files + force => true, # purge subdirs and links + require => File["${vardir}/"], + } + + # if we manually *pick* a uuid, then store it too, so that it + # sticks if we ever go back to using automatic uuids. this is + # useful if a user wants to initially import uuids by picking + # them manually, and then letting puppet take over afterwards + file { "${vardir}/uuid/uuid": + content => "${uuid}" ? { + '' => undef, + default => "${uuid}\n", + }, + owner => root, + group => root, + mode => 600, # might as well... + ensure => present, + require => File["${vardir}/uuid/"], + } + + $valid_uuid = "${uuid}" ? { + # fact from the data generated in: ${vardir}/uuid/uuid + '' => "${::gluster_uuid}", + default => "${uuid}", + } + if "${valid_uuid}" == '' { + fail('No valid UUID exists yet!') + } else { + # get shorter version string for loose matching... + $gluster_main_version = regsubst( + "${gluster_version}", # eg: 3.4.0 + '^(\d+)\.(\d+)\.(\d+)$', # int.int.int + '\1.\2' # print int.int + ) + + # TODO: add additional values to this table... + $operating_version = "${gluster_version}" ? { + '' => '', # gluster not yet installed... + # specific version matches go here... + '3.4.0' => '2', + default => "${gluster_main_version}" ? { + # loose version matches go here... + #'3.3' => '1', # blank... + '3.4' => '2', + #'3.5' => '3', # guessing... + default => '-1', # unknown... + }, + } + + # this catches unknown gluster versions to add to table + if "${operating_version}" == '-1' { + warning("Gluster version '${gluster_version}' is unknown.") + } + + # set a unique uuid per host, and operating version... + file { '/var/lib/glusterd/glusterd.info': + content => template('gluster/glusterd.info.erb'), + owner => root, + group => root, + mode => 600, # u=rw,go=r + seltype => 'glusterd_var_lib_t', + seluser => 'system_u', + ensure => present, + notify => Service["${::gluster::params::service_glusterd}"], + require => File['/var/lib/glusterd/'], + } + + # NOTE: $name here should probably be the fqdn... + @@file { "${vardir}/uuid/uuid_${name}": + content => "${valid_uuid}\n", + tag => 'gluster_uuid', + owner => root, + group => root, + mode => 600, + ensure => present, + } + } + + File <<| tag == 'gluster_uuid' |>> { # collect to make facts + } + + } else { + $valid_uuid = "${uuid}" ? { + # fact from the data generated in: ${vardir}/uuid/uuid + '' => getvar("gluster_uuid_${name}"), # fact ! + default => "${uuid}", + } + if "${valid_uuid}" == '' { + notice('No valid UUID exists yet.') # different msg + } else { + # set uuid= + exec { "/bin/echo 'uuid=${valid_uuid}' >> '/var/lib/glusterd/peers/${valid_uuid}'": + logoutput => on_failure, + unless => "/bin/grep -qF 'uuid=' '/var/lib/glusterd/peers/${valid_uuid}'", + notify => [ + # propagate the notify up + File['/var/lib/glusterd/peers/'], + Service["${::gluster::params::service_glusterd}"], # ensure reload + ], + before => File["/var/lib/glusterd/peers/${valid_uuid}"], + alias => "gluster-host-uuid-${name}", + # FIXME: doing this causes a dependency cycle! adding + # the Package[] require doesn't. It would be most + # correct to require the peers/ folder, but since it's + # not working, requiring the Package[] will still give + # us the same result. (Package creates peers/ folder). + # NOTE: it's possible the cycle is a bug in puppet or a + # bug in the dependencies somewhere else in this module. + #require => File['/var/lib/glusterd/peers/'], + require => Package["${::gluster::params::package_glusterfs_server}"], + } + + # set state= + exec { "/bin/echo 'state=3' >> '/var/lib/glusterd/peers/${valid_uuid}'": + logoutput => on_failure, + unless => "/bin/grep -qF 'state=' '/var/lib/glusterd/peers/${valid_uuid}'", + notify => [ + # propagate the notify up + File['/var/lib/glusterd/peers/'], + Service["${::gluster::params::service_glusterd}"], # ensure reload + ], + before => File["/var/lib/glusterd/peers/${valid_uuid}"], + require => Exec["gluster-host-uuid-${name}"], + alias => "gluster-host-state-${name}", + } + + # set hostname1=... + exec { "/bin/echo 'hostname1=${name}' >> '/var/lib/glusterd/peers/${valid_uuid}'": + logoutput => on_failure, + unless => "/bin/grep -qF 'hostname1=' '/var/lib/glusterd/peers/${valid_uuid}'", + notify => [ + # propagate the notify up + File['/var/lib/glusterd/peers/'], + Service["${::gluster::params::service_glusterd}"], # ensure reload + ], + before => File["/var/lib/glusterd/peers/${valid_uuid}"], + require => Exec["gluster-host-state-${name}"], + } + + # tag the file so it doesn't get removed by purge + file { "/var/lib/glusterd/peers/${valid_uuid}": + ensure => present, + owner => root, + group => root, + # NOTE: this mode was found by inspecting the process + mode => 600, # u=rw,go=r + seltype => 'glusterd_var_lib_t', + seluser => 'system_u', + notify => [ + # propagate the notify up + File['/var/lib/glusterd/peers/'], + Service["${::gluster::params::service_glusterd}"], # ensure reload + ], + } + } + } + + # vrrp... + $vrrp = $::gluster::server::vrrp + if ( "${fqdn}" == "${name}" ) and $vrrp { + + $vip = $::gluster::server::vip + if ! ($vip =~ /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) { + fail('You must specify a valid VIP to use with VRRP.') + } + + file { "${vardir}/vrrp/": + ensure => directory, # make sure this is a directory + recurse => true, # recurse into directory + purge => true, # purge unmanaged files + force => true, # purge subdirs and links + require => File["${vardir}/"], + } + + # store so that a fact can figure out the interface and cidr... + file { "${vardir}/vrrp/ip": + content => "${valid_ip}\n", + owner => root, + group => root, + mode => 600, # might as well... + ensure => present, + require => File["${vardir}/vrrp/"], + } + + # NOTE: this is a tag to protect the pass file... + file { "${vardir}/vrrp/vrrp": + content => "${password}" ? { + '' => undef, + default => "${password}", + }, + owner => root, + group => root, + mode => 600, # might as well... + ensure => present, + require => File["${vardir}/vrrp/"], + } + + # NOTE: $name here should probably be the fqdn... + @@file { "${vardir}/vrrp/vrrp_${name}": + content => "${::gluster_vrrp}\n", + tag => 'gluster_vrrp', + owner => root, + group => root, + mode => 600, + ensure => present, + } + + File <<| tag == 'gluster_vrrp' |>> { # collect to make facts + } + + # this figures out the interface from the $valid_ip value + $if = "${::gluster_vrrp_interface}" # a smart fact! + $cidr = "${::gluster_vrrp_cidr}" # even smarter! + $p = "${::gluster::server::password}" ? { # shh secret... + '' => "${::gluster_vrrp_password}", # combined fact + default => "${::gluster::server::password}", + } + # this fact is sorted, which is very, very important...! + $fqdns_fact = "${::gluster_vrrp_fqdns}" # fact ! + $fqdns = split($fqdns_fact, ',') # list ! + + if "${if}" != '' and "${cidr}" != '' and "${p}" != '' { + + keepalived::vrrp { 'VI_GLUSTER': # TODO: groups! + state => "${fqdns[0]}" ? { # first in list + '' => 'MASTER', # list is empty + "${fqdn}" => 'MASTER', # we are first! + default => 'BACKUP', # other in list + }, + interface => "${if}", + mcastsrc => "${valid_ip}", + # TODO: support configuring the label index! + # label ethX:1 for first VIP ethX:2 for second... + ipaddress => "${vip}/${cidr} dev ${if} label ${if}:1", + # FIXME: this limits puppet-gluster to 256 hosts maximum + priority => inline_template("<%= 255 - (@fqdns.index('${fqdn}') or 0) %>"), + routerid => 42, # TODO: support configuring it! + advertint => 3, # TODO: support configuring it! + password => "${p}", + #group => 'gluster', # TODO: groups! + watchip => "${vip}", + shorewall_zone => "${::gluster::server::zone}", + shorewall_ipaddress => "${valid_ip}", + } + } + } + + # firewalling... + $shorewall = $::gluster::server::shorewall + if ( "${fqdn}" == "${name}" ) and $shorewall { + $zone = $::gluster::server::zone # firewall zone + $ips = $::gluster::server::ips # override host ip list + + #$other_host_ips = inline_template("<%= ips.delete_if {|x| x == '${ipaddress}' }.join(',') %>") # list of ips except myself + #$all_ips = inline_template("<%= (ips+[vip]+clients).uniq.delete_if {|x| x.empty? }.join(',') %>") + $source_ips = type($ips) ? { + 'array' => inline_template("<%= (ips+[]).uniq.delete_if {|x| x.empty? }.join(',') %>"), + default => ["${valid_ip}"], + } + + @@shorewall::rule { "glusterd-management-${name}": + action => 'ACCEPT', + source => "${zone}", # override this on collect... + source_ips => $source_ips, + dest => '$FW', + proto => 'tcp', + port => '24007', + comment => 'Allow incoming tcp:24007 from each glusterd.', + tag => 'gluster_firewall_management', + ensure => present, + } + + # NOTE: used by rdma + @@shorewall::rule { "glusterd-rdma-${name}": + action => 'ACCEPT', + source => "${zone}", # override this on collect... + source_ips => $source_ips, + dest => '$FW', + proto => 'tcp', + port => '24008', + comment => 'Allow incoming tcp:24008 for rdma.', + tag => 'gluster_firewall_management', + ensure => present, + } + + # TODO: is this only used for nfs? + @@shorewall::rule { "gluster-tcp111-${name}": + action => 'ACCEPT', + source => "${zone}", # override this on collect... + source_ips => $source_ips, + dest => '$FW', + proto => 'tcp', + port => '111', + comment => 'Allow tcp 111.', + tag => 'gluster_firewall_management', + ensure => present, + } + + # TODO: is this only used for nfs? + # TODO: johnmark says gluster nfs udp doesn't work :P + @@shorewall::rule { "gluster-udp111-${name}": + action => 'ACCEPT', + source => "${zone}", # override this on collect... + source_ips => $source_ips, + dest => '$FW', + proto => 'udp', + port => '111', + comment => 'Allow udp 111.', + tag => 'gluster_firewall_management', + ensure => present, + } + + # TODO: this collects our own entries too... we could exclude + # them but this isn't a huge issue at the moment... + Shorewall::Rule <<| tag == 'gluster_firewall_management' |>> { + source => "${zone}", # use our source zone + before => Service["${::gluster::params::service_glusterd}"], + } + } +} + +# vim: ts=8 diff --git a/gluster/manifests/host/data.pp b/gluster/manifests/host/data.pp new file mode 100644 index 000000000..dad0b3733 --- /dev/null +++ b/gluster/manifests/host/data.pp @@ -0,0 +1,26 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::host::data( + #$name, + $ip, + $fqdn +) { + # so far, this does nothing but 'store' variables +} + +# vim: ts=8 diff --git a/gluster/manifests/init.pp b/gluster/manifests/init.pp new file mode 100644 index 000000000..952843fbb --- /dev/null +++ b/gluster/manifests/init.pp @@ -0,0 +1,63 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# +# NOTES +# + +# * To rebuild gluster (erasing all data), rm -rf the storage dirs to +# clear metadata. To do this without erasing data, read this article: +# http://joejulian.name/blog/glusterfs-path-or-a-prefix-of-it-is-already-part-of-a-volume/ +# +# * List of state codes: +# +# static char *glusterd_friend_sm_state_names[] = { # glusterd-sm.c +# "Establishing Connection", # 0 +# "Probe Sent to Peer", # 1 +# "Probe Received from Peer", # 2 +# "Peer in Cluster", # 3 (verified) +# "Accepted peer request", # 4 +# "Sent and Received peer request", # 5 +# "Peer Rejected", # 6 (verified) +# "Peer detach in progress", # 7 +# "Probe Received from peer", # 8 +# "Connected to Peer", # 9 +# "Peer is connected and Accepted", # 10 +# "Invalid State" # 11 +# }; +# +# * To use this gluster module, it's recommended that all nodes receive +# the same puppet configuration. Puppet is smart enough to know what to +# run on each participating node. Watchout for the mild race condition. +# +# * TODO: add more notes... + +# +# XXX: FIXME: TODO +# +# XXX: does parted align disks properly ? +# XXX: mkfs.xfs -ssize=4k /dev/sdc1 ? # should "-s sector_size" be used ? http://kb.lsi.com/KnowledgebaseArticle16187.aspx ? +# XXX: setup auth somehow... ip address based for now # XXX: use volume::property... + +# FIXME: test this: https://bugzilla.redhat.com/show_bug.cgi?id=GLUSTER-3769 +# FIXME: peering: maybe we can just specify a guid somewhere so that everyone peers together ? +# FIXME: can we setup gluster by using templated volume files instead ? + +# TODO: package { 'xfsdump': ensure => present } is this useful for something ? +# TODO: find out when ports are actually necessary for version 3.3 + +# vim: ts=8 diff --git a/gluster/manifests/mount.pp b/gluster/manifests/mount.pp new file mode 100644 index 000000000..2e842b1cd --- /dev/null +++ b/gluster/manifests/mount.pp @@ -0,0 +1,180 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# XXX: try mounting with: glusterfs --volfile-server= --volfile-id= --xlator-option='*dht*.assert-no-child-down=yes' # TODO: quotes or not? +define gluster::mount( + $server, # NOTE: use a vip as server hostname + $rw = false, # mount read only (true) or rw (false) +# $suid = false, # mount with suid (true) or nosuid (false) # TODO: will this work with gluster ? + $mounted = true, # useful if we want to pull in the group + # defs, but not actually mount (testing) + $repo = true, # add a repo automatically? true or false + $version = '', # pick a specific version (defaults to latest) + $ip = '', # you can specify which ip address to use (if multiple) + $shorewall = false +) { + include gluster::params + + #mount -t glusterfs brick1.example.com:/test /test + #include gluster::mount::base + #class { '::gluster::mount::base': + # repo => $repo, + # version => $version, + #} + $params = { + 'repo' => $repo, + 'version' => $version, + } + # because multiple gluster::mount types are allowed on the same server, + # we include with the ensure_resource function to avoid identical calls + ensure_resource('class', 'gluster::mount::base', $params) + + # eg: vip:/volume + $split = split($server, ':') # do some $server parsing + $host = $split[0] # host fqdn or ip (eg: vip) + # NOTE: technically $path should be everything BUT split[0]. This + # lets our $path include colons if for some reason they're needed. + #$path = $split[1] # volume + # TODO: create substring function + $path = inline_template("<%= '${server}'.slice('${host}'.length+1, '${server}'.length-'${host}'.length-1) %>") + $short_path = sprintf("%s", regsubst($path, '\/$', '')) # no trailing + #$valid_path = sprintf("%s/", regsubst($path, '\/$', '')) + $volume = sprintf("%s", regsubst($short_path, '^\/', '')) # no leading + + if ! ( "${host}:${path}" == "${server}" ) { + fail('The $server must match a $host:$path pattern.') + } + + if ! ( "${host}:/${volume}" == "${server}" ) { + fail('The $server must match a $host:/$volume pattern.') + } + + $short_name = sprintf("%s", regsubst("${name}", '\/$', '')) # no trailing + $long_name = sprintf("%s/", regsubst("${name}", '\/$', '')) # trailing... + + $valid_ip = "${ip}" ? { + '' => "${::gluster_host_ip}" ? { # smart fact... + '' => "${::ipaddress}", # puppet picks! + default => "${::gluster_host_ip}", # smart + }, + default => "${ip}", # user selected + } + if "${valid_ip}" == '' { + fail('No valid IP exists!') + } + + if $shorewall { + $safename = regsubst("${name}", '/', '_', 'G') # make /'s safe + @@shorewall::rule { "glusterd-management-${fqdn}-${safename}": + #@@shorewall::rule { "glusterd-management-${volume}-${fqdn}": + action => 'ACCEPT', + source => '', # override this on collect... + source_ips => ["${valid_ip}"], + dest => '$FW', + proto => 'tcp', + port => '24007', + comment => 'Allow incoming tcp:24007 from each glusterd.', + tag => 'gluster_firewall_management', + ensure => present, + } + + # wrap shorewall::rule in a fake type so that we can add $match + #@@shorewall::rule { "gluster-volume-${fqdn}-${safename}": + @@gluster::rulewrapper { "gluster-volume-${fqdn}-${safename}": + action => 'ACCEPT', + source => '', # override this on collect... + source_ips => ["${valid_ip}"], + dest => '$FW', + proto => 'tcp', + port => '', # override this on collect... + #comment => "${fqdn}", + comment => 'Allow incoming tcp port from glusterfsds.', + tag => 'gluster_firewall_volume', + match => "${volume}", # used for collection + ensure => present, + } + } + + $rw_bool = $rw ? { + true => 'rw', + default => 'ro', + } + + # TODO: will this work with gluster ? + #$suid_bool = $suid ? { + # true => 'suid', + # default => 'nosuid', + #} + + $mounted_bool = $mounted ? { + false => unmounted, + default => mounted, + } + + # ensure parent directories exist + exec { "gluster-mount-mkdir-${name}": + command => "/bin/mkdir -p '${long_name}'", + creates => "${long_name}", + logoutput => on_failure, + before => File["${long_name}"], + } + + # make an empty directory for the mount point + file { "${long_name}": # ensure a trailing slash + ensure => directory, # make sure this is a directory + recurse => false, # don't recurse into directory + purge => false, # don't purge unmanaged files + force => false, # don't purge subdirs and links + alias => "${short_name}", # don't allow duplicates name's + } + + $packages = "${::gluster::params::package_glusterfs_fuse}" ? { + '' => ["${::gluster::params::package_glusterfs}"], + default => [ + "${::gluster::params::package_glusterfs}", + "${::gluster::params::package_glusterfs_fuse}", + ], + } + # Mount Options: + # * backupvolfile-server=server-name + # * fetch-attempts=N (where N is number of attempts) + # * log-level=loglevel + # * log-file=logfile + # * direct-io-mode=[enable|disable] + # * ro (for readonly mounts) + # * acl (for enabling posix-ACLs) + # * worm (making the mount WORM - Write Once, Read Many type) + # * selinux (enable selinux on GlusterFS mount) + # XXX: consider mounting only if some exported resource, collected and turned into a fact shows that the volume is available... + # XXX: or something... consider adding the notify => Poke[] functionality + mount { "${short_name}": + atboot => true, + ensure => $mounted_bool, + device => "${server}", + fstype => 'glusterfs', + options => "defaults,_netdev,${rw_bool}", # TODO: will $suid_bool work with gluster ? + dump => '0', # fs_freq: 0 to skip file system dumps + pass => '0', # fs_passno: 0 to skip fsck on boot + require => [ + Package[$packages], + File["${long_name}"], # the mountpoint + Exec['gluster-fuse'], # ensure fuse is loaded! + ], + } +} + +# vim: ts=8 diff --git a/gluster/manifests/mount/base.pp b/gluster/manifests/mount/base.pp new file mode 100644 index 000000000..38bdd8552 --- /dev/null +++ b/gluster/manifests/mount/base.pp @@ -0,0 +1,97 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::mount::base( + $repo = true, # add a repo automatically? true or false + $version = '' # pick a specific version (defaults to latest) +) { + include gluster::vardir + include gluster::params + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + # if we use ::mount and ::server on the same machine, this could clash, + # so we use the ensure_resource function to allow identical duplicates! + $rname = "${version}" ? { + '' => 'gluster', + default => "gluster-${version}", + } + if $repo { + $params = { + 'version' => "${version}", + } + ensure_resource('gluster::repo', "${rname}", $params) + } + + $packages = "${::gluster::params::package_glusterfs_fuse}" ? { + '' => ["${::gluster::params::package_glusterfs}"], + default => [ + "${::gluster::params::package_glusterfs}", + "${::gluster::params::package_glusterfs_fuse}", + ], + } + package { $packages: + ensure => "${version}" ? { + '' => present, + default => "${version}", + }, + before => "${::gluster::params::package_glusterfs_api}" ? { + '' => undef, + default => Package["${::gluster::params::package_glusterfs_api}"], + }, + require => $repo ? { + false => undef, + default => Gluster::Repo["${rname}"], + }, + } + + $api_params = { + 'repo' => $repo, + 'version' => "${version}", + } + ensure_resource('class', 'gluster::api', $api_params) + + # FIXME: choose a reliable and correct way to ensure fuse is loaded + # dmesg | grep -i fuse + # modprobe fuse + # dmesg | grep -i fuse + #fuse init (API version 7.13) + # + + # modprobe fuse if it's missing + exec { "${::gluster::params::program_modprobe} fuse": + logoutput => on_failure, + onlyif => '/usr/bin/test -z "`/bin/dmesg | /bin/grep -i fuse`"', + alias => 'gluster-fuse', + } + #exec { "${::gluster::params::program_modprobe} fuse": + # logoutput => on_failure, + # unless => "${::gluster::params::program_lsmod} | /bin/grep -q '^fuse'", + # alias => 'gluster-modprobe-fuse', + #} + + # TODO: will this autoload the fuse module? + #file { '/etc/modprobe.d/fuse.conf': + # content => "fuse\n", # TODO: "install fuse ${::gluster::params::program_modprobe} --ignore-install fuse ; /bin/true\n" ? + # owner => root, + # group => root, + # mode => 644, # u=rw,go=r + # ensure => present, + #} +} + +# vim: ts=8 diff --git a/gluster/manifests/params.pp b/gluster/manifests/params.pp new file mode 100644 index 000000000..8ed265e3c --- /dev/null +++ b/gluster/manifests/params.pp @@ -0,0 +1,98 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::params( + # packages... + $package_glusterfs = 'glusterfs', + $package_glusterfs_fuse = 'glusterfs-fuse', + $package_glusterfs_server = 'glusterfs-server', + $package_glusterfs_api = 'glusterfs-api', + + $package_e2fsprogs = 'e2fsprogs', + $package_xfsprogs = 'xfsprogs', + $package_btrfsprogs = 'btrfs-progs', + + $package_python_argparse = 'python-argparse', + $package_python_lxml = 'python-lxml', + $package_fping = 'fping', + + # programs... + $program_gluster = '/usr/sbin/gluster', + + $program_modprobe = '/sbin/modprobe', + $program_lsmod = '/sbin/lsmod', + + $program_parted = '/sbin/parted', + $program_pvcreate = '/sbin/pvcreate', + $program_vgcreate = '/sbin/vgcreate', + $program_lvcreate = '/sbin/lvcreate', + $program_vgs = '/sbin/vgs', + $program_lvs = '/sbin/lvs', + $program_pvdisplay = '/sbin/pvdisplay', + $program_vgdisplay = '/sbin/vgdisplay', + #$program_lvdisplay = '/sbin/lvdisplay', + $program_xfsadmin = '/usr/sbin/xfs_admin', + $program_mkfs_xfs = '/sbin/mkfs.xfs', + $program_mkfs_ext4 = '/sbin/mkfs.ext4', + $program_mkfs_btrfs = '/sbin/mkfs.btrfs', + + $program_fping = '/usr/sbin/fping', + $program_findmnt = '/bin/findmnt', + + # services... + $service_glusterd = 'glusterd', + + # external modules... + $include_puppet_facter = true, + + # misc... + $misc_gluster_reload = '/sbin/service glusterd reload', + $misc_gluster_repo = 'https://download.gluster.org/pub/gluster/glusterfs/', + + # comment... + $comment = '' +) { + if "${comment}" == '' { + warning('Unable to load yaml data/ directory!') + } + + $valid_include_puppet_facter = $include_puppet_facter ? { + true => true, + false => false, + 'true' => true, + 'false' => false, + default => true, + } + + if $valid_include_puppet_facter { + include puppet::facter + $factbase = "${::puppet::facter::base}" + $hash = { + 'gluster_program_gluster' => $program_gluster, + } + # create a custom external fact! + file { "${factbase}gluster_program.yaml": + content => inline_template('<%= @hash.to_yaml %>'), + owner => root, + group => root, + mode => 644, # u=rw,go=r + ensure => present, + } + } +} + +# vim: ts=8 diff --git a/gluster/manifests/repo.pp b/gluster/manifests/repo.pp new file mode 100644 index 000000000..57b8db66f --- /dev/null +++ b/gluster/manifests/repo.pp @@ -0,0 +1,121 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +define gluster::repo( + # if you specify 'x.y', it will find the latest x.y.* + # if you specify 'x.y.z', it will stick to that version + # anything omitted is taken to mean "latest" + # if you leave this blank, we assume you want the latest version... + $version = '' +) { + include gluster::params + + $base = "${::gluster::params::misc_gluster_repo}" + + if "${version}" == '' { + # latest + $base_v = "${base}LATEST/" + } else { + notice("GlusterFS version: '${version}' was chosen.") + + # parse out the -release if it exists. example: 3.4.2-13.el6 + # \1 is the major/minor version, eg: 3.4.2 + # \2 is the release with a leading dash, eg: -13.el6 + # \3 is the first part of the release, eg: 13 + # \4 is the second part of the release, eg: el6 + $real_v = regsubst("${version}", '^([\d\.]*)(\-([\d]{1,})\.([a-zA-Z\d]{1,}))?$', '\1') + + # search for qa style releases + $qa_pattern = '^([\d\.]*)(\-(0\.[\d]{1,}\.((alpha|beta|qa|rc)[\d]{1,}))\.([a-zA-Z\d]{1,}))?$' + $qa_type = regsubst("${version}", "${qa_pattern}", '\5') + $qa = "${qa_type}" ? { + /(alpha|beta|qa|rc)/ => true, + default => false, + } + + if $qa { + $qa_folder = regsubst("${version}", "${qa_pattern}", '\1\4') + # look inside the qa-releases/ subfolder... + $base_v = "${base}qa-releases/${qa_folder}/" + + } elsif "${real_v}" =~ /^(\d+)\.(\d+)$/ { # x.y + #$base_v = "${base}${1}.${2}/LATEST/" # same! + $base_v = "${base}${real_v}/LATEST/" + + } elsif "${real_v}" =~ /^(\d+)\.(\d+)\.(\d+)$/ { # x.y.z + #$base_v = "${base}${1}.${2}/${1}.${2}.${3}/" # same! + $base_v = "${base}${1}.${2}/${real_v}/" + + } else { + fail('The version string is invalid.') + } + } + + case $operatingsystem { + 'CentOS': { + $base_os = "${base_v}CentOS/" + } + 'RedHat': { + $base_os = "${base_v}RHEL/" + } + #'Debian', 'Ubuntu': { + #} + default: { + fail("Operating system: '${operatingsystem}' not yet supported.") + } + } + + $arch = "${architecture}" ? { + 'x86_64' => 'x86_64', + 'i386' => 'i386', + 'i486' => 'i386', + 'i586' => 'i386', + 'i686' => 'i386', + default => '', + } + if "${arch}" == '' { + fail("Architecture: '${architecture}' not yet supported.") + } + + $base_arch = "${base_os}epel-${operatingsystemrelease}/" + + $gpgkey = "${base_os}pub.key" + + include ::yum + + #yum::repos::repo { "gluster-${arch}": + yum::repos::repo { "${name}": + baseurl => "${base_arch}${arch}/", + enabled => true, + gpgcheck => true, + # XXX: this should not be an https:// link, it should be a file + gpgkeys => ["${gpgkey}"], + ensure => present, + } + + # TODO: technically, i don't think this is needed yet... + #yum::repos::repo { 'gluster-noarch': + # baseurl => "${base_arch}noarch/", + # enabled => true, + # gpgcheck => true, + # # XXX: this should not be an https:// link, it should be a file + # gpgkeys => ["${gpgkey}"], + # ensure => present, + #} +} + +# vim: ts=8 diff --git a/gluster/manifests/rulewrapper.pp b/gluster/manifests/rulewrapper.pp new file mode 100644 index 000000000..4ac641989 --- /dev/null +++ b/gluster/manifests/rulewrapper.pp @@ -0,0 +1,47 @@ +# GlusterFS module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this wraps shorewall::rule so that we can add on additional fake 'tags' +define gluster::rulewrapper( + $action = '', + $source = '', + $source_ips = [], + $dest = '', + $dest_ips = [], + $proto = '', + $port = [], + $sport = [], + $original = [], + $comment = '', + $ensure = present, + $match = '' # additional tag parameter +) { + shorewall::rule { "${name}": + action => "${action}", + source => "${source}", + source_ips => $source_ips, + dest => "${dest}", + dest_ips => $dest_ips, + proto => "${proto}", + port => $port, + sport => $sport, + comment => "${comment}", + ensure => $ensure, + } +} + +# vim: ts=8 diff --git a/gluster/manifests/server.pp b/gluster/manifests/server.pp new file mode 100644 index 000000000..d0650919a --- /dev/null +++ b/gluster/manifests/server.pp @@ -0,0 +1,186 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::server( + $nfs = false, # TODO + $vip = '', # vip of the cluster (optional but recommended) + $vrrp = false, + $password = '', # global vrrp password to use + $version = '', # pick a specific version (defaults to latest) + $repo = true, # add a repo automatically? true or false + $baseport = '', # specify base port option as used in glusterd.vol file + $rpcauthallowinsecure = false, # needed in some setups in glusterd.vol + $shorewall = false, + $zone = 'net', # TODO: allow a list of zones + $ips = false, # an optional list of ip's for each in hosts[] + $clients = [] # list of allowed client ip's # TODO: get from exported resources +) { + $FW = '$FW' # make using $FW in shorewall easier + + include gluster::vardir + include gluster::params + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + # if we use ::mount and ::server on the same machine, this could clash, + # so we use the ensure_resource function to allow identical duplicates! + $rname = "${version}" ? { + '' => 'gluster', + default => "gluster-${version}", + } + if $repo { + $params = { + 'version' => "${version}", + } + ensure_resource('gluster::repo', "${rname}", $params) + } + + # this is meant to be replace the excellent sponge utility by sponge.py + file { "${vardir}/sponge.py": # for scripts needing: 'sponge' + source => 'puppet:///modules/gluster/sponge.py', + owner => root, + group => nobody, + mode => 700, # u=rwx + backup => false, # don't backup to filebucket + ensure => present, + before => Package["${::gluster::params::package_glusterfs_server}"], + require => File["${vardir}/"], + } + + package { "${::gluster::params::package_glusterfs_server}": + ensure => "${version}" ? { + '' => present, + default => "${version}", + }, + before => "${::gluster::params::package_glusterfs_api}" ? { + '' => undef, + default => Package["${::gluster::params::package_glusterfs_api}"], + }, + require => $repo ? { + false => undef, + default => Gluster::Repo["${rname}"], + }, + } + + $api_params = { + 'repo' => $repo, + 'version' => "${version}", + } + ensure_resource('class', 'gluster::api', $api_params) + + # NOTE: not that we necessarily manage anything in here at the moment... + file { '/etc/glusterfs/': + ensure => directory, # make sure this is a directory + recurse => false, # TODO: eventually... + purge => false, # TODO: eventually... + force => false, # TODO: eventually... + owner => root, + group => root, + mode => 644, + #notify => Service["${::gluster::params::service_glusterd}"], # TODO: ??? + require => Package["${::gluster::params::package_glusterfs_server}"], + } + + # NOTE: this option can be useful for users of libvirt migration as in: + # https://bugzilla.redhat.com/show_bug.cgi?id=987555 + $valid_baseport = inline_template('<%= [Fixnum, String].include?(@baseport.class) ? @baseport.to_i : 0 %>') + + $valid_rpcauthallowinsecure = $rpcauthallowinsecure ? { + true => true, + default => false, + } + + file { '/etc/glusterfs/glusterd.vol': + content => template('gluster/glusterd.vol.erb'), + owner => root, + group => root, + mode => 644, # u=rw,go=r + ensure => present, + require => File['/etc/glusterfs/'], + } + + file { '/var/lib/glusterd/': + ensure => directory, # make sure this is a directory + recurse => false, # TODO: eventually... + purge => false, # TODO: eventually... + force => false, # TODO: eventually... + owner => root, + group => root, + mode => 644, + #notify => Service["${::gluster::params::service_glusterd}"], # TODO: eventually... + require => File['/etc/glusterfs/glusterd.vol'], + } + + file { '/var/lib/glusterd/peers/': + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, + force => true, + owner => root, + group => root, + mode => 644, + notify => Service["${::gluster::params::service_glusterd}"], + require => File['/var/lib/glusterd/'], + } + + if $vrrp { + class { '::keepalived': + start => true, + shorewall => $shorewall, + } + } + + if $shorewall { + # XXX: WIP + #if type($ips) == 'array' { + # #$other_host_ips = inline_template("<%= ips.delete_if {|x| x == '${ipaddress}' }.join(',') %>") # list of ips except myself + # $source_ips = inline_template("<%= (ips+clients).uniq.delete_if {|x| x.empty? }.join(',') %>") + # #$all_ips = inline_template("<%= (ips+[vip]+clients).uniq.delete_if {|x| x.empty? }.join(',') %>") + + # $src = "${source_ips}" ? { + # '' => "${zone}", + # default => "${zone}:${source_ips}", + # } + + #$endport = inline_template('<%= 24009+hosts.count %>') + #$nfs_endport = inline_template('<%= 38465+hosts.count %>') + #shorewall::rule { 'gluster-24000': + # rule => " + # ACCEPT ${src} $FW tcp 24009:${endport} + # ", + # comment => 'Allow 24000s for gluster', + # before => Service["${::gluster::params::service_glusterd}"], + #} + + #if $nfs { # FIXME: TODO + # shorewall::rule { 'gluster-nfs': rule => " + # ACCEPT $(src} $FW tcp 38465:${nfs_endport} + # ", comment => 'Allow nfs for gluster'} + #} + } + + # start service only after the firewall is opened and hosts are defined + service { "${::gluster::params::service_glusterd}": + enable => true, # start on boot + ensure => running, # ensure it stays running + hasstatus => false, # FIXME: BUG: https://bugzilla.redhat.com/show_bug.cgi?id=836007 + hasrestart => true, # use restart, not start; stop + } +} + +# vim: ts=8 diff --git a/gluster/manifests/simple.pp b/gluster/manifests/simple.pp new file mode 100644 index 000000000..1b72df3ba --- /dev/null +++ b/gluster/manifests/simple.pp @@ -0,0 +1,189 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::simple( + $path = '', + $volume = 'puppet', # NOTE: this can be a list... + $replica = 1, + $stripe = 1, # TODO: not fully implemented in puppet-gluster + $layout = '', # brick layout to use (default, chained, etc...) + $vip = '', # strongly recommended + $vrrp = false, + $password = '', # global vrrp password to use + $version = '', + $repo = true, + $count = 0, # 0 means build 1 brick, unless $brick_params exists... + $brick_params = {}, # this sets the brick count when $count is 0... + $brick_param_defaults = {}, # these always get used to build bricks + $brick_params_defaults = [], # array of hashes to use as brick count + $setgroup = '', # pick a volume property group to set, eg: virt + $ping = true, # use fping or not? + $again = true, # do we want to use Exec['again'] ? + $baseport = '', # specify base port option as used in glusterd.vol file + $rpcauthallowinsecure = false, # needed in some setups in glusterd.vol + $shorewall = true +) { + include gluster::vardir + include gluster::volume::property::group::data # make the groups early + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + if "${path}" == '' { + file { "${vardir}/data/": + ensure => directory, # make sure this is a directory + recurse => false, # don't recurse into directory + purge => false, # don't purge unmanaged files + force => false, # don't purge subdirs and links + require => File["${vardir}/"], + } + } + + $chosen_path = "${path}" ? { + '' => "${vardir}/data/", + default => "${path}", + } + + $valid_path = sprintf("%s/", regsubst($chosen_path, '\/$', '')) + + $valid_volumes = type($volume) ? { # always an array of volumes... + 'array' => $volume, + default => ["${volume}"], + } + + # if this is a hash, then it's used as the defaults for all the bricks! + validate_hash($brick_param_defaults) + # if someone explicitly added this value, then don't overwrite it... + $areyousure = {'areyousure' => true} + $againhash = {'again' => $again} # pass through the $again value + $valid_brick_param_defaults = merge($areyousure, $againhash, $brick_param_defaults) + + # if this is an array, then each element is the default for each brick! + # if this is an array, then the number of elements is the brick count!! + validate_array($brick_params_defaults) + # TODO: check that each element of array is a valid hash! + $valid_brick_params_defaults = $brick_params_defaults + + notify { 'gluster::simple': + message => 'You are using gluster::simple !', + } + + if "${vip}" == '' { + # If you don't use a VIP, things will be racy, but could mostly + # work. If you run puppet manually, then a vip isn't necessary. + # see: http://ttboj.wordpress.com/2012/08/23/how-to-avoid-cluster-race-conditions-or-how-to-implement-a-distributed-lock-manager-in-puppet/ + warning('It is highly recommended to use a VIP.') + } + + class { '::gluster::server': + vip => "${vip}", + vrrp => $vrrp, + password => "${password}", + version => "${version}", + repo => $repo, + baseport => $baseport, + rpcauthallowinsecure => $rpcauthallowinsecure, + #zone => 'net', # defaults to net + shorewall => $shorewall, + } + + if "${::fqdn}" == '' { + fail('Your $fqdn is empty. Please check your DNS settings.') + } + + @@gluster::host { "${::fqdn}": + } + Gluster::Host <<||>> + + # the idea here is to build a list of bricks from a list of parameters, + # with each element in the list, corresponding to a hash of key=>values + # each element in the list is a different brick. the key for the master + # hash is the fqdn of the host that those bricks correspond to. you can + # also specify a list of defaults for when you have common brick values + # such as $xfs_inode64=>true, or raid_* if your cluster is symmetrical! + # if you set the $count variable, then that brick count will be forced. + validate_re("${count}", '^\d+$') # ensure this is a positive int + + # here some wizardry happens... + if has_key($brick_params, "${::fqdn}") { + $brick_params_list = $brick_params["${::fqdn}"] + } else { + $brick_params_list = [] + } + + $brick_params_list_length = inline_template('<%= @brick_params_list.length %>') + $brick_params_defaults_length = inline_template('<%= @valid_brick_params_defaults.length %>') + $valid_count = "${count}" ? { + '0' => "${brick_params_list_length}" ? { + '0' => "${brick_params_defaults_length}" ? { + '0' => 1, # 0 means undefined, so use the default + default => "${brick_params_defaults_length}", + }, + default => "${brick_params_list_length}", + }, + default => $count, + } + + $brick_params_names = "${valid_count}" ? { + # TODO: should we use the same pattern for 1 or many ? + '1' => ["${::fqdn}:${valid_path}"], + default => split(inline_template("<%= (1..@valid_count.to_i).collect{|i| '${::fqdn}:${valid_path}brick' + i.to_s.rjust(7, '0') + '/' }.join(',') %>"), ','), + } + + validate_array($brick_params_list) + + # NOTE: I've kept this template split as two comment chunks for + # readability. Puppet needs to fix this issue somehow! Creating + # a separate template removes the logic from the code, but as a + # large inline template it's very hard to read/write the logic! + #DEFAULTS = (((i < @valid_brick_params_defaults.length) and @valid_brick_params_defaults[i].is_a?(Hash)) ? @valid_brick_params_defaults[i] : {}) + #$yaml = inline_template("<%= (0..@valid_count.to_i-1).inject(Hash.new) { |h,i| {@brick_params_names[i] => DEFAULTS.merge((i < @brick_params_list.length) ? @brick_params_list[i] : {})}.merge(h) }.to_yaml %>") + $yaml = inline_template("<%= (0..@valid_count.to_i-1).inject(Hash.new) { |h,i| {@brick_params_names[i] => (((i < @valid_brick_params_defaults.length) and @valid_brick_params_defaults[i].is_a?(Hash)) ? @valid_brick_params_defaults[i] : {}).merge((i < @brick_params_list.length) ? @brick_params_list[i] : {})}.merge(h) }.to_yaml %>") + $hash = parseyaml($yaml) + create_resources('@@gluster::brick', $hash, $valid_brick_param_defaults) + #@@gluster::brick { "${::fqdn}:${valid_path}": + # areyousure => true, + #} + Gluster::Brick <<||>> + + gluster::volume { $valid_volumes: + replica => $replica, + stripe => $stripe, + layout => "${layout}", + # NOTE: with this method you do not choose the order of course! + # the gluster_fqdns fact is alphabetical, but not complete till + # at least a puppet run of each node has occured. watch out for + # partial clusters missing some of the nodes with bad ordering! + #bricks => split(inline_template("<%= @gluster_fqdns.split(',').collect {|x| x+':${valid_path}' }.join(',') %>"), ','), + # the only semi-safe way is the new built in automatic collect: + bricks => true, # automatic brick collection... + ping => $ping, + start => true, + again => $again, + } + Gluster::Volume <<||>> + + # set a group of volume properties + if "${setgroup}" != '' { + $setgroup_yaml = inline_template("<%= @valid_volumes.inject(Hash.new) { |h,i| {i+'#'+@setgroup => {}}.merge(h) }.to_yaml %>") + $setgroup_hash = parseyaml($setgroup_yaml) + $setgroup_defaults = {'vip' => "${vip}"} + create_resources('gluster::volume::property::group', $setgroup_hash, $setgroup_defaults) + } +} + +# vim: ts=8 diff --git a/gluster/manifests/vardir.pp b/gluster/manifests/vardir.pp new file mode 100644 index 000000000..2dd40d500 --- /dev/null +++ b/gluster/manifests/vardir.pp @@ -0,0 +1,52 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::vardir { # module vardir snippet + if "${::puppet_vardirtmp}" == '' { + if "${::puppet_vardir}" == '' { + # here, we require that the puppetlabs fact exist! + fail('Fact: $puppet_vardir is missing!') + } + $tmp = sprintf("%s/tmp/", regsubst($::puppet_vardir, '\/$', '')) + # base directory where puppet modules can work and namespace in + file { "${tmp}": + ensure => directory, # make sure this is a directory + recurse => false, # don't recurse into directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, + group => nobody, + mode => 600, + backup => false, # don't backup to filebucket + #before => File["${module_vardir}"], # redundant + #require => Package['puppet'], # no puppet module seen + } + } else { + $tmp = sprintf("%s/", regsubst($::puppet_vardirtmp, '\/$', '')) + } + $module_vardir = sprintf("%s/gluster/", regsubst($tmp, '\/$', '')) + file { "${module_vardir}": # /var/lib/puppet/tmp/gluster/ + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${tmp}"], # File['/var/lib/puppet/tmp/'] + } +} + +# vim: ts=8 diff --git a/gluster/manifests/volume.pp b/gluster/manifests/volume.pp new file mode 100644 index 000000000..b203482b0 --- /dev/null +++ b/gluster/manifests/volume.pp @@ -0,0 +1,442 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +define gluster::volume( + $bricks = true, # specify a list of bricks, or true for auto... + $group = 'default', # use this bricks group name if we auto collect + $transport = 'tcp', + $replica = 1, + $stripe = 1, + # TODO: maybe this should be called 'chained' => true/false, and maybe, + # we can also specify an offset count for chaining, or other parameters + $layout = '', # brick layout to use (default, chained, etc...) + $vip = '', # vip of the cluster (optional but recommended) + $ping = true, # do we want to include fping checks ? + $settle = true, # do we want to run settle checks ? + $again = true, # do we want to use Exec['again'] ? + $start = undef # start volume ? true, false (stop it) or undef +) { + include gluster::xml + if $again { + include gluster::again + } + include gluster::vardir + include gluster::params + include gluster::volume::base + if $ping { + include gluster::volume::ping + } + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + $shorewall = $::gluster::server::shorewall + + $settle_count = 3 # three is a reasonable default! + $maxlength = 3 + if $maxlength < $settle_count { + fail('The $maxlength needs to be greater than or equal to the $settle_count.') + } + $are_bricks_collected = (type($bricks) == 'boolean' and ($bricks == true)) + # NOTE: settle checks are still useful even if we are using ping checks + # the reason why they are still useful, is that they can detect changes + # in the bricks, which might propagate slowly because of exported types + # the fping checks can only verify that the individual hosts are alive! + $settle_count_check = $are_bricks_collected ? { + false => false, + default => $settle ? { + false => false, + default => true, + } + } + # TODO: implement settle_time_check + $settle_time_check = $settle ? { + false => false, + default => true, + } + + # clean up old fsm data when not in use, because parent $vardir purges! + if $are_bricks_collected { + include gluster::volume::fsm + file { "${vardir}/volume/fsm/${name}/": + ensure => directory, # make sure this is a directory + recurse => true, # recurse into directory + purge => false, # don't purge unmanaged files + force => false, # don't purge subdirs and links + require => File["${vardir}/volume/fsm/"], + } + } + + $gluster_brick_group_fact = getvar("gluster_brick_group_${group}") + $collected_bricks = split($gluster_brick_group_fact, ',') + # run the appropriate layout function here + $ordered_brick_layout = $layout ? { + 'chained' => brick_layout_chained($replica, $collected_bricks), + default => brick_layout_simple($replica, $collected_bricks), + } + + $valid_bricks = type($bricks) ? { + 'boolean' => $bricks ? { + true => $ordered_brick_layout, # an array... + default => [], # invalid type + }, + 'array' => $bricks, + default => [], # invalid type + } + + # helpful debugging! + notice(inline_template('collected_bricks: <%= @collected_bricks.inspect %>')) + notice(inline_template('valid_bricks: <%= @valid_bricks.inspect %>')) + + # NOTE: we're using the valid_bricks value here, and not the collected + # value. while we only need the $collected value for settle detection, + # the actual brick value is needed for future add/remove brick code... + $valid_input = join($valid_bricks, ',') # TODO: this should be pickled + if $are_bricks_collected and "${valid_input}" == '' { + notice('The gluster::brick collection is not ready yet.') + } + $last = getvar("gluster_volume_fsm_state_${name}") # fact ! + $valid_last = "${last}" ? { + # initialize the $last var to match the $input if it's empty... + '' => "${valid_input}", + default => "${last}", + } + if $are_bricks_collected and ("${valid_input}" != '') and ("${valid_last}" == '') { + fail('Previous state is invalid.') # fact was tampered with + } + + # NOTE: the stack lists are left base64 encoded since we only care if they change! :P + # NOTE: each element in stack is base64 encoded because they contain: , + $stack_fact = getvar("gluster_volume_fsm_stack_${name}") # fact ! + $stack_full = split("${stack_fact}", ',') + $stack_trim = "${maxlength}" ? { + '-1' => $stack_full, # unlimited + #default => split(inline_template('<%= @stack_full[0,@maxlength.to_i.abs].join(",") %>'), ','), + default => split(inline_template('<%= @stack_full[[@stack_full.size-@maxlength.to_i.abs,0].max,@maxlength.to_i.abs].join(",") %>'), ','), + } + + $watch_fact = getvar("gluster_volume_fsm_watch_${name}") # fact ! + $watch_full = split("${watch_fact}", ',') + $watch_trim = "${maxlength}" ? { + '-1' => $watch_full, # unlimited + #default => split(inline_template('<%= @watch_full[0,@maxlength.to_i.abs].join(",") %>'), ','), + default => split(inline_template('<%= @watch_full[[@watch_full.size-@maxlength.to_i.abs,0].max,@maxlength.to_i.abs].join(",") %>'), ','), + } + + # if the last $settle_count elements are the same, the template + # should reduce down to the value '1'. check this and the size. + $one = inline_template('<%= @watch_trim[[@watch_trim.size-@settle_count.to_i,0].max,@settle_count.to_i].uniq.size %>') + $watch_trim_size = size($watch_trim) + $settled = ((! $settle_count_check) or ((size($watch_trim) >= $settle_count) and "${one}" == '1')) + + # TODO: if using rdma, maybe we should pull in the rdma package... ? + $valid_transport = $transport ? { + 'rdma' => 'rdma', + 'tcp,rdma' => 'tcp,rdma', + default => 'tcp', + } + + $valid_replica = $replica ? { + '1' => '', + default => "replica ${replica} ", + } + + $valid_stripe = $stripe ? { + '1' => '', + default => "stripe ${stripe} ", + } + + $valid_vip = "${vip}" ? { + '' => $::gluster::server::vip, + default => "${vip}", + } + + # returns interface name that has vip, or '' if none are found. + $vipif = inline_template("<%= @interfaces.split(',').find_all {|x| '${valid_vip}' == scope.lookupvar('ipaddress_'+x) }[0,1].join('') %>") + + #Gluster::Brick[$valid_bricks] -> Gluster::Volume[$name] # volume requires bricks + + # get the bricks that match our fqdn, and append /$name to their path. + # return only these paths, which can be used to build the volume dirs. + # NOTE: gluster v3.4 won't create a volume if this dir already exists. + # TODO: is this needed when bricks are devices and not on filesystem ? + #$volume_dirs = split(inline_template("<%= @valid_bricks.find_all{|x| x.split(':')[0] == '${fqdn}' }.collect {|y| y.split(':')[1].chomp('/')+'/${name}' }.join(' ') %>"), ' ') + #file { $volume_dirs: + # ensure => directory, # make sure this is a directory + # recurse => false, # don't recurse into directory + # purge => false, # don't purge unmanaged files + # force => false, # don't purge subdirs and links + # before => Exec["gluster-volume-create-${name}"], + # require => Gluster::Brick[$valid_bricks], + #} + + # add /${name} to the end of each: brick:/path entry + $brick_spec = inline_template("<%= @valid_bricks.collect {|x| ''+x.chomp('/')+'/${name}' }.join(' ') %>") + + # if volume creation fails for a stupid reason, in many cases, glusterd + # already did some of the work and left us with volume name directories + # on all bricks. the problem is that the future volume create commands, + # will error if they see that volume directory already present, so when + # we error we should rmdir any empty volume dirs to keep it pristine... + # TODO: this should be a gluster bug... we must hope it doesn't happen! + # maybe related to: https://bugzilla.redhat.com/show_bug.cgi?id=835494 + $rmdir_volume_dirs = sprintf("/bin/rmdir '%s'", inline_template("<%= @valid_bricks.find_all{|x| x.split(':')[0] == '${fqdn}' }.collect {|y| y.split(':')[1].chomp('/')+'/${name}/' }.join('\' \'') %>")) + + # get the list of bricks fqdn's that don't have our fqdn + $others = inline_template("<%= @valid_bricks.find_all{|x| x.split(':')[0] != '${fqdn}' }.collect {|y| y.split(':')[0] }.join(' ') %>") + + $fping = sprintf("${::gluster::params::program_fping} -q %s", $others) + $status = sprintf("${::gluster::params::program_gluster} peer status --xml | ${vardir}/xml.py connected %s", $others) + + $onlyif = $ping ? { + false => "${status}", + default => [ + "${fping}", + "${status}", + ], + } + + $require = $ping ? { + false => [ + Service["${::gluster::params::service_glusterd}"], + File["${vardir}/volume/create-${name}.sh"], + File["${vardir}/xml.py"], # status check + Gluster::Brick[$valid_bricks], + Exec["gluster-volume-stuck-${name}"], + ], + default => [ + Service["${::gluster::params::service_glusterd}"], + File["${vardir}/volume/create-${name}.sh"], + Package["${::gluster::params::package_fping}"], + File["${vardir}/xml.py"], # status check + Gluster::Brick[$valid_bricks], + Exec["gluster-volume-stuck-${name}"], + ], + } + + # work around stuck connection state (4) of: 'Accepted peer request'... + exec { "gluster-volume-stuck-${name}": + command => "${::gluster::params::misc_gluster_reload}", + logoutput => on_failure, + unless => "${::gluster::params::program_gluster} volume list | /bin/grep -qxF '${name}' -", # reconnect if it doesn't exist + onlyif => sprintf("${::gluster::params::program_gluster} peer status --xml | ${vardir}/xml.py stuck %s", $others), + notify => $again ? { + false => undef, + default => Common::Again::Delta['gluster-exec-again'], + }, + require => [ + Service["${::gluster::params::service_glusterd}"], + File["${vardir}/xml.py"], # stuck check + Gluster::Brick[$valid_bricks], + ], + } + + # store command in a separate file to run as bash... + # NOTE: we sleep for 5 seconds to give glusterd a chance to + # settle down first if we're doing a hot (clean) puppet run + # NOTE: force is needed for now because of the following error: + # volume create: puppet: failed: The brick annex1.example.com:/var/lib/puppet/tmp/gluster/data/puppet is is being created in the root partition. It is recommended that you don't use the system's root partition for storage backend. Or use 'force' at the end of the command if you want to override this behavior. + # FIXME: it would be create to have an --allow-root-storage type option + # instead, so that we don't inadvertently force some other bad thing... + file { "${vardir}/volume/create-${name}.sh": + content => inline_template("#!/bin/bash\n/bin/sleep 5s && ${::gluster::params::program_gluster} volume create ${name} ${valid_replica}${valid_stripe}transport ${valid_transport} ${brick_spec} force > >(/usr/bin/tee '/tmp/gluster-volume-create-${name}.stdout') 2> >(/usr/bin/tee '/tmp/gluster-volume-create-${name}.stderr' >&2) || (${rmdir_volume_dirs} && /bin/false)\nexit \$?\n"), + owner => root, + group => root, + mode => 755, + ensure => present, + # this notify is the first to kick off the 2nd step! it + # was put here after a process of elimination, and this + # location makes a lot of sense: on change exec[again]! + notify => $again ? { + false => undef, + default => Common::Again::Delta['gluster-exec-again'], + }, + require => File["${vardir}/volume/"], + } + + # run if vip not defined (bypass mode) or if vip exists on this machine + if ("${valid_vip}" == '' or "${vipif}" != '') { + + # NOTE: This should only happen on one host! + # NOTE: There's maybe a theoretical race condition if this runs + # at exactly the same time on more than one host. That's why it + # is advisable to use a vip. + # NOTE: This could probably fail on at least N-1 nodes (without + # vip) or one (the vip node, when using vip) before it succeeds + # because it shouldn't work until all the bricks are available, + # which per node will happen right before this runs. + # fping all the other nodes to ensure they're up for creation + # TODO: consider piping in a /usr/bin/yes to avoid warnings... + # NOTE: in this command, we save the std{out,err} and pass them + # on too for puppet to consume. we save in /tmp for fast access + # EXAMPLE: gluster volume create test replica 2 transport tcp annex1.example.com:/storage1a/test annex2.example.com:/storage2a/test annex3.example.com:/storage3b/test annex4.example.com:/storage4b/test annex1.example.com:/storage1c/test annex2.example.com:/storage2c/test annex3.example.com:/storage3d/test annex4.example.com:/storage4d/test + # TODO: add a timer similar to my puppet-runonce timer code... to wait X minutes after we've settled... useful if we Exec['again'] to speed things up... + if $settled { + exec { "gluster-volume-create-${name}": + command => "${vardir}/volume/create-${name}.sh", + logoutput => on_failure, + unless => "${::gluster::params::program_gluster} volume list | /bin/grep -qxF '${name}' -", # add volume if it doesn't exist + onlyif => $onlyif, + #before => TODO?, + require => $require, + alias => "gluster-volume-create-${name}", + } + } + + if $start == true { + # try to start volume if stopped + exec { "${::gluster::params::program_gluster} volume start ${name}": + logoutput => on_failure, + onlyif => "${::gluster::params::program_gluster} volume list | /bin/grep -qxF '${name}' -", + unless => "${::gluster::params::program_gluster} volume status ${name}", # returns false if stopped + notify => $shorewall ? { + false => undef, + default => $again ? { + false => undef, + default => Common::Again::Delta['gluster-exec-again'], + }, + }, + require => $settled ? { # require if type exists + false => undef, + default => Exec["gluster-volume-create-${name}"], + }, + alias => "gluster-volume-start-${name}", + } + } elsif ( $start == false ) { + # try to stop volume if running + # NOTE: this will still succeed even if a client is mounted + # NOTE: This uses `yes` to workaround the: Stopping volume will + # make its data inaccessible. Do you want to continue? (y/n) + # TODO: http://community.gluster.org/q/how-can-i-make-automatic-scripts/ + # TODO: gluster --mode=script volume stop ... + exec { "/usr/bin/yes | ${::gluster::params::program_gluster} volume stop ${name}": + logoutput => on_failure, + onlyif => "${::gluster::params::program_gluster} volume status ${name}", # returns true if started + require => $settled ? { # require if type exists + false => undef, + default => Exec["gluster-volume-create-${name}"], + }, + alias => "gluster-volume-stop-${name}", + } + } else { # 'undef'-ined + # don't manage volume run state + } + } + + if $shorewall { + $zone = $::gluster::server::zone # firewall zone + + $ips = $::gluster::server::ips # override host ip list + $ip = $::gluster::host::data::ip # ip of brick's host... + $source_ips = type($ips) ? { + 'array' => inline_template("<%= (@ips+[]).uniq.delete_if {|x| x.empty? }.join(',') %>"), + default => ["${ip}"], + } + + $port = getvar("gluster_ports_volume_${name}") # fact ! + + # NOTE: we need to add the $fqdn so that exported resources + # don't conflict... I'm not sure they should anyways though + @@shorewall::rule { "gluster-volume-${name}-${fqdn}": + action => 'ACCEPT', + source => "${zone}", # override this on collect... + source_ips => $source_ips, + dest => '$FW', + proto => 'tcp', + port => "${port}", # comma separated string or list + #comment => "${fqdn}", + comment => 'Allow incoming tcp port from glusterfsds.', + tag => 'gluster_firewall_volume', + ensure => present, + } + # we probably shouldn't collect the above rule from our self... + #Shorewall::Rule <<| tag == 'gluster_firewall_volume' and comment != "${fqdn}" |>> { + Shorewall::Rule <<| tag == 'gluster_firewall_volume' |>> { + source => "${zone}", # use our source zone + before => Service["${::gluster::params::service_glusterd}"], + } + + Gluster::Rulewrapper <<| tag == 'gluster_firewall_volume' and match == "${name}" |>> { + #Shorewall::Rule <<| tag == 'gluster_firewall_volume' and match == "${name}" |>> { + source => "${zone}", # use our source zone + port => "${port}", # comma separated string or list + before => Service["${::gluster::params::service_glusterd}"], + } + } + + # fsm variables and boilerplate + $statefile = "${vardir}/volume/fsm/${name}/state" + $stackfile = "${vardir}/volume/fsm/${name}/stack" + $watchfile = "${vardir}/volume/fsm/${name}/watch" + $diff = "/usr/bin/test '${valid_input}' != '${valid_last}'" + $stack_truncate = "${maxlength}" ? { + '-1' => '', # unlimited + #default => sprintf("&& /bin/sed -i '%d,$ d' ${stackfile}", inline_template('<%= @maxlength.to_i.abs+1 %>')), + default => sprintf(" && (/bin/grep -v '^$' ${stackfile} | /usr/bin/tail -n %d | ${vardir}/sponge.py ${stackfile})", inline_template('<%= @maxlength.to_i.abs %>')), + } + $watch_truncate = "${maxlength}" ? { + '-1' => '', # unlimited + #default => sprintf("&& /bin/sed -i '%d,$ d' ${watchfile}", inline_template('<%= @maxlength.to_i.abs+1 %>')), + default => sprintf(" && (/bin/grep -v '^$' ${watchfile} | /usr/bin/tail -n %d | ${vardir}/sponge.py ${watchfile})", inline_template('<%= @maxlength.to_i.abs %>')), + } + + if $are_bricks_collected and ("${valid_input}" != '') { # ready or not? + + # TODO: future versions should pickle (but with yaml) + exec { "/bin/echo '${valid_input}' > '${statefile}'": + logoutput => on_failure, + onlyif => "/usr/bin/test ! -e '${statefile}' || ${diff}", + require => File["${vardir}/volume/fsm/${name}/"], + alias => "gluster-volume-fsm-state-${name}", + } + + # NOTE: keep a stack of past transitions, and load them in as a list... + exec { "/bin/echo '${valid_input}' >> '${stackfile}'${stack_truncate}": + logoutput => on_failure, + onlyif => "/usr/bin/test ! -e '${stackfile}' || ${diff}", + require => [ + File["${vardir}/volume/fsm/${name}/"], + # easy way to ensure the transition types don't need to + # add a before to both exec's since this one follows it + Exec["gluster-volume-fsm-state-${name}"], + ], + alias => "gluster-volume-fsm-stack-${name}", + } + + # NOTE: watch *all* transitions, and load them in as a list... + exec { "/bin/echo '${valid_input}' >> '${watchfile}'${watch_truncate}": + logoutput => on_failure, + # we run this if the file doesn't exist, or there is a + # difference to record, or the sequence hasn't settled + # we also check that we have our minimum settle count! + onlyif => "/usr/bin/test ! -e '${watchfile}' || ${diff} || /usr/bin/test '1' != '${one}' || /usr/bin/test ${watch_trim_size} -lt ${settle_count}", + notify => $again ? { + false => undef, + default => Common::Again::Delta['gluster-exec-again'], + }, + require => [ + File["${vardir}/volume/fsm/${name}/"], + # easy way to ensure the transition types don't need to + # add a before to both exec's since this one follows it + Exec["gluster-volume-fsm-state-${name}"], + ], + alias => "gluster-volume-fsm-watch-${name}", + } + } +} + +# vim: ts=8 diff --git a/gluster/manifests/volume/base.pp b/gluster/manifests/volume/base.pp new file mode 100644 index 000000000..1991ce738 --- /dev/null +++ b/gluster/manifests/volume/base.pp @@ -0,0 +1,32 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::volume::base { + + include gluster::vardir + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + file { "${vardir}/volume/": + ensure => directory, # make sure this is a directory + recurse => true, # don't recurse into directory + purge => true, # don't purge unmanaged files + force => true, # don't purge subdirs and links + require => File["${vardir}/"], + } +} +# vim: ts=8 diff --git a/gluster/manifests/volume/fsm.pp b/gluster/manifests/volume/fsm.pp new file mode 100644 index 000000000..c780ede6d --- /dev/null +++ b/gluster/manifests/volume/fsm.pp @@ -0,0 +1,32 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::volume::fsm { + + include gluster::vardir + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + file { "${vardir}/volume/fsm/": + ensure => directory, # make sure this is a directory + recurse => true, # don't recurse into directory + purge => true, # don't purge unmanaged files + force => true, # don't purge subdirs and links + require => File["${vardir}/volume/"], + } +} +# vim: ts=8 diff --git a/gluster/manifests/volume/ping.pp b/gluster/manifests/volume/ping.pp new file mode 100644 index 000000000..d2ec4a2f9 --- /dev/null +++ b/gluster/manifests/volume/ping.pp @@ -0,0 +1,28 @@ +# GlusterFS module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::volume::ping { + + include gluster::params + + # for checking other bricks are up + package { "${::gluster::params::package_fping}": + ensure => present, + } +} + +# vim: ts=8 diff --git a/gluster/manifests/volume/property.pp b/gluster/manifests/volume/property.pp new file mode 100644 index 000000000..923e39495 --- /dev/null +++ b/gluster/manifests/volume/property.pp @@ -0,0 +1,222 @@ +# GlusterFS module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: thanks to Joe Julian for: http://community.gluster.org/q/what-is-the-command-that-someone-can-run-to-get-the-value-of-a-given-property/ + +define gluster::volume::property( + $value, + $vip = '', # vip of the cluster (optional but recommended) + $autotype = true # set to false to disable autotyping +) { + include gluster::xml + include gluster::vardir + include gluster::params + include gluster::volume::property::data + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + $split = split($name, '#') # do some $name parsing + $volume = $split[0] # volume name + $key = $split[1] # key name + + if ! ( "${volume}#${key}" == "${name}" ) { + fail('The property $name must match a $volume#$key pattern.') + } + + # split out $etype and $jchar lookup into a separate file + $etypes = $::gluster::volume::property::data::etypes + $jchars = $::gluster::volume::property::data::jchars + + # transform our etypes into short etypes (missing the prefix) + # TODO: we should see if there are duplicates (collisions) + # if there are collisions, and a gluster volume set group type contains + # one of these keys, then it's ambiguous and it's clearly a gluster bug + $short_etypes_yaml = inline_template('<%= @etypes.inject({}) {|h, (x,y)| h[ (x.index(".").nil?? x : x[x.index(".")+1..-1]) ] = y; h }.to_yaml %>') + $short_jchars_yaml = inline_template('<%= @jchars.inject({}) {|h, (x,y)| h[ (x.index(".").nil?? x : x[x.index(".")+1..-1]) ] = y; h }.to_yaml %>') + $short_etypes = parseyaml($short_etypes_yaml) + $short_jchars = parseyaml($short_jchars_yaml) + + # FIXME: a short key should lookup the equivalent in the normal table, + # and vice-versa, and set an alias so that you can't define a short + # key and a long key at the same time which refer to the same variable! + + # expected type + if has_key($etypes, "${key}") { + $etype = $etypes["${key}"] ? { + '' => 'undefined', + default => $etypes["${key}"], + } + # the keys of these etypes are missing their prefix up to the first '.' + } elsif has_key($short_etypes, "${key}") { + $etype = $short_etypes["${key}"] ? { + '' => 'undefined', + default => $short_etypes["${key}"], + } + } else { + $etype = 'undefined' + } + + if (! $autotype) { + if type($value) != 'string' { + fail('Expecting type(string) if autotype is disabled.') + } + $safe_value = shellquote($value) # TODO: is this the safe thing? + + # if it's a special offon type and of an acceptable value + } elsif ($etype == 'offon') { # default is off + if type($value) == 'boolean' { + $safe_value = $value ? { + true => 'on', + default => 'off', + } + + } elsif type($value) == 'string' { + $downcase_value = inline_template('<%= @value.downcase %>') + $safe_value = $downcase_value ? { + 'on' => 'on', + #'off' => 'off', + default => 'off', + } + + } else { + fail("Gluster::Volume::Property[${key}] must be type: ${etype}.") + } + + # if it's a special onoff type and of an acceptable value + } elsif ($etype == 'onoff') { # default is on + if type($value) == 'boolean' { + $safe_value = $value ? { + false => 'off', + default => 'on', + } + + } elsif type($value) == 'string' { + $downcase_value = inline_template('<%= @value.downcase %>') + $safe_value = $downcase_value ? { + 'off' => 'off', + #'on' => 'on', + default => 'on', + } + + } else { + fail("Gluster::Volume::Property[${key}] must be type: ${etype}.") + } + + # if it's a special truefalse type and of an acceptable value + } elsif ($etype == 'truefalse') { # default is true + if type($value) == 'boolean' { + $safe_value = $value ? { + false => 'false', + default => 'true', + } + + } elsif type($value) == 'string' { + $downcase_value = inline_template('<%= @value.downcase %>') + $safe_value = $downcase_value ? { + 'false' => 'false', + default => 'true', + } + + } else { + fail("Gluster::Volume::Property[${key}] must be type: ${etype}.") + } + + # if it's a special falsetrue type and of an acceptable value + } elsif ($etype == 'falsetrue') { # default is false + if type($value) == 'boolean' { + $safe_value = $value ? { + true => 'true', + default => 'false', + } + + } elsif type($value) == 'string' { + $downcase_value = inline_template('<%= @value.downcase %>') + $safe_value = $downcase_value ? { + 'true' => 'true', + default => 'false', + } + + } else { + fail("Gluster::Volume::Property[${key}] must be type: ${etype}.") + } + + } elsif $etype == 'integer' { + # TODO: we could also add range and/or set validation + $safe_value = inline_template('<%= [Fixnum, String].include?(@value.class) ? @value.to_i : "null" %>') + if "${safe_value}" == 'null' { # value was of an invalid type! + fail("Gluster::Volume::Property[${key}] must be type: ${etype}.") + } + + } elsif $etype == 'string' { + $safe_value = shellquote($value) # TODO: is this the safe thing? + + # if it's not a string and it's not the expected type, fail + } elsif ( type($value) != $etype ) { # type() from puppetlabs-stdlib + fail("Gluster::Volume::Property[${key}] must be type: ${etype}.") + + # convert to correct type + } else { + + if $etype == 'string' { + $safe_value = shellquote($value) # TODO: is this the safe thing? + } elsif $etype == 'array' { + + # join char + if has_key($jchars, "${key}") { + $jchar = $jchars["${key}"] + } elsif has_key($short_jchars, "${key}") { + $jchar = $short_jchars["${key}"] + } else { + $jchar = '' + } + + $safe_value = inline_template('<%= @value.join(jchar) %>') + #} elsif ... { # TODO: add more conversions here if needed + + } else { + fail("Unknown type: ${etype}.") + } + } + + $valid_vip = "${vip}" ? { + '' => $::gluster::server::vip, + default => "${vip}", + } + + # returns interface name that has vip, or '' if none are found. + $vipif = inline_template("<%= @interfaces.split(',').find_all {|x| '${valid_vip}' == scope.lookupvar('ipaddress_'+x) }[0,1].join('') %>") + + # run if vip not defined (bypass mode) or if vip exists on this machine + if ("${valid_vip}" == '' or "${vipif}" != '') { + # volume set + # set a volume property only if value doesn't match what is available + # FIXME: check that the value we're setting isn't the default + # FIXME: you can check defaults with... gluster volume set help | ... + exec { "${::gluster::params::program_gluster} volume set ${volume} ${key} ${safe_value}": + unless => "/usr/bin/test \"`${::gluster::params::program_gluster} volume --xml info ${volume} | ${vardir}/xml.py property --key '${key}'`\" = '${safe_value}'", + onlyif => "${::gluster::params::program_gluster} volume list | /bin/grep -qxF '${volume}' -", + logoutput => on_failure, + require => [ + Gluster::Volume[$volume], + File["${vardir}/xml.py"], + ], + } + } +} + +# vim: ts=8 diff --git a/gluster/manifests/volume/property/data.pp b/gluster/manifests/volume/property/data.pp new file mode 100644 index 000000000..99c29c78d --- /dev/null +++ b/gluster/manifests/volume/property/data.pp @@ -0,0 +1,336 @@ +# GlusterFS module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::volume::property::data() { + + # expected type + $etypes = { + + # Allow a comma separated list of addresses and/or hostnames to connect to the server. By default, all connections are allowed. + 'auth.allow' => 'array', # default: (null) + + # Reject a comma separated list of addresses and/or hostnames to connect to the server. By default, all connections are allowed. + 'auth.reject' => 'array', # default: (null) + + # This specifies the number of self-heals that can be performed in background without blocking the fop + 'cluster.background-self-heal-count' => 'integer', # default: 16 + + # Choose a local subvolume (i.e. Brick) to read from if read-subvolume is not explicitly set. + 'cluster.choose-local' => 'truefalse', # default: true + + # Data fops like write/truncate will not perform pre/post fop changelog operations in afr transaction if this option is disabled + 'cluster.data-change-log' => 'onoff', # default: on + + # Using this option we can enable/disable data self-heal on the file. "open" means data self-heal action will only be triggered by file open operations. + 'cluster.data-self-heal' => 'onoff', # default: on + + # Select between "full", "diff". The "full" algorithm copies the entire file from source to sink. The "diff" algorithm copies to sink only those blocks whose checksums don't match with those of source. If no option is configured the option is chosen dynamically as follows: If the file does not exist on one of the sinks or empty file exists or if the source file size is about the same as page size the entire file will be read and written i.e "full" algo, otherwise "diff" algo is chosen. + 'cluster.data-self-heal-algorithm' => 'string', # default: (reset) + + # Lock phase of a transaction has two sub-phases. First is an attempt to acquire locks in parallel by broadcasting non-blocking lock requests. If lock acquisition fails on any server, then the held locks are unlocked and revert to a blocking locked mode sequentially on one server after another. If this option is enabled the initial broadcasting lock request attempt to acquire lock on the entire file. If this fails, we revert back to the sequential "regional" blocking lock as before. In the case where such an "eager" lock is granted in the non-blocking phase, it gives rise to an opportunity for optimization. i.e, if the next write transaction on the same FD arrives before the unlock phase of the first transaction, it "takes over" the full file lock. Similarly if yet another data transaction arrives before the unlock phase of the "optimized" transaction, that in turn "takes over" the lock as well. The actual unlock now happens at the end of the last "optimized" transaction. + 'cluster.eager-lock' => 'onoff', # default: on + + # Entry fops like create/unlink will not perform pre/post fop changelog operations in afr transaction if this option is disabled + 'cluster.entry-change-log' => 'onoff', # default: on + + # Using this option we can enable/disable entry self-heal on the directory. + 'cluster.entry-self-heal' => 'onoff', # default: on + + # time interval for checking the need to self-heal in self-heal-daemon + 'cluster.heal-timeout' => 'integer', # default: 600 + + # This option if set to ON, does a lookup through all the sub-volumes, in case a lookup didn't return any result from the hash subvolume. If set to OFF, it does not do a lookup on the remaining subvolumes. + 'cluster.lookup-unhashed' => 'onoff', # default: on + + # Metadata fops like setattr/setxattr will not perform pre/post fop changelog operations in afr transaction if this option is disabled + 'cluster.metadata-change-log' => 'onoff', # default: on + + # Using this option we can enable/disable metadata i.e. Permissions, ownerships, xattrs self-heal on the file/directory. + 'cluster.metadata-self-heal' => 'onoff', # default: on + + # Percentage/Size of disk space, after which the process starts balancing out the cluster, and logs will appear in log files + 'cluster.min-free-disk' => 'string', # default: 10% + + # after system has only N% of inodes, warnings starts to appear in log files + 'cluster.min-free-inodes' => 'string', # default: 5% + + # If quorum-type is "fixed" only allow writes if this many bricks or present. Other quorum types will OVERWRITE this value. + 'cluster.quorum-count' => 'integer', # default: (null) + + # If value is "fixed" only allow writes if quorum-count bricks are present. If value is "auto" only allow writes if more than half of bricks, or exactly half including the first, are present. + 'cluster.quorum-type' => 'string', # default: none + + # readdir(p) will not failover if this option is off + 'cluster.readdir-failover' => 'onoff', # default: on + + # This option if set to ON enables the optimization that allows DHT to requests non-first subvolumes to filter out directory entries. + 'cluster.readdir-optimize' => 'offon', # default: off + + # inode-read fops happen only on one of the bricks in replicate. AFR will prefer the one computed using the method specified using this option0 = first responder, 1 = hash by GFID of file (all clients use same subvolume), 2 = hash by GFID of file and client PID + 'cluster.read-hash-mode' => 'integer', # default: 0 + + # inode-read fops happen only on one of the bricks in replicate. Afr will prefer the one specified using this option if it is not stale. Option value must be one of the xlator names of the children. Ex: -client-0 till -client- + 'cluster.read-subvolume' => 'string', # default: (null) + + # inode-read fops happen only on one of the bricks in replicate. AFR will prefer the one specified using this option if it is not stale. allowed options include -1 till replica-count - 1 + 'cluster.read-subvolume-index' => 'integer', # default: -1 + + # This option if set to ON displays and logs the time taken for migration of each file, during the rebalance process. If set to OFF, the rebalance logs will only display the time spent in each directory. + 'cluster.rebalance-stats' => 'offon', # default: off + + # This option applies to only self-heal-daemon. Index directory crawl and automatic healing of files will not be performed if this option is turned off. + 'cluster.self-heal-daemon' => 'offon', # default: off + + # readdirp size for performing entry self-heal + 'cluster.self-heal-readdir-size' => 'integer', # default: 1024 - Min 1024 Max 131072 + + # Maximum number blocks per file for which self-heal process would be applied simultaneously. + 'cluster.self-heal-window-size' => 'integer', # default: 1 + + # Sets the quorum percentage for the trusted storage pool. + 'cluster.server-quorum-ratio' => 'integer', # in % default: (null) + + # If set to server, enables the specified volume to participate in quorum. + 'cluster.server-quorum-type' => 'string', # default: (null) + + # Size of the stripe unit that would be read from or written to the striped servers. + 'cluster.stripe-block-size' => 'string', # default: 128KB + + # Enable coalesce mode to flatten striped files as stored on the server (i.e., eliminate holes caused by the traditional format). + 'cluster.stripe-coalesce' => 'falsetrue', # default: false + + # Specifies the directory layout spread. + 'cluster.subvols-per-directory' => 'integer', # default: (null) + + # Changes the log-level of the bricks + 'diagnostics.brick-log-level' => 'string', # default: INFO + + # Gluster's syslog log-level + 'diagnostics.brick-sys-log-level' => 'string', # default: CRITICAL + + # Changes the log-level of the clients + 'diagnostics.client-log-level' => 'string', # default: INFO + + # Gluster's syslog log-level + 'diagnostics.client-sys-log-level' => 'string', # default: CRITICAL + + # If on stats related to file-operations would be tracked inside GlusterFS data-structures. + 'diagnostics.dump-fd-stats' => 'offon', # default: off + + # If on stats related to the latency of each operation would be tracked inside GlusterFS data-structures. + 'diagnostics.latency-measurement' => 'offon', # default: off + + # Sets the grace-timeout value. Valid range 10-1800. + 'features.grace-timeout' => 'integer', # default: (null) + + # Enables or disables the lock heal. + 'features.lock-heal' => 'offon', # default: off + + # quota caches the directory sizes on client. Timeout indicates the timeout for the cache to be revalidated. + 'features.quota-timeout' => 'integer', # default: 0 + + # Time frame after which the (file) operation would be declared as dead, if the server does not respond for a particular (file) operation. + 'network.frame-timeout' => 'integer', # default: 1800 + + # Specifies the maximum megabytes of memory to be used in the inode cache. + 'network.inode-lru-limit' => 'integer', # default: 16384 + + # Time duration for which the client waits to check if the server is responsive. + 'network.ping-timeout' => 'integer', # default: 42 + + # If enabled, in open() and creat() calls, O_DIRECT flag will be filtered at the client protocol level so server will still continue to cache the file. This works similar to NFS's behavior of O_DIRECT + 'network.remote-dio' => 'string', # default: disable + + # XXX: this appears twice + # Specifies the window size for tcp socket. + 'network.tcp-window-size' => 'integer', # default: (null) + + # Users have the option of turning on name lookup for incoming client connections using this option. Use this option to turn on name lookups during address-based authentication. Turning this on will enable you to use hostnames in rpc-auth.addr.* filters. In some setups, the name server can take too long to reply to DNS queries resulting in timeouts of mount requests. By default, name lookup is off + 'nfs.addr-namelookup' => 'offon', # default: (off) + + # This option is used to start or stop NFS server for individual volume. + 'nfs.disable' => 'offon', # default: (off) + + # Internal option set to tell gnfs to use a different scheme for encoding file handles when DVM is being used. + 'nfs.dynamic-volumes' => 'offon', # default: (off) + + # For nfs clients or apps that do not support 64-bit inode numbers, use this option to make NFS return 32-bit inode numbers instead. Disabled by default, so NFS returns 64-bit inode numbers. + 'nfs.enable-ino32' => 'offon', # default: (off) + + # By default, all subvolumes of nfs are exported as individual exports. There are cases where a subdirectory or subdirectories in the volume need to be exported separately. This option can also be used in conjunction with nfs3.export-volumes option to restrict exports only to the subdirectories specified through this option. Must be an absolute path. + 'nfs.export-dir' => 'string', # default: (null) + + # By default, all subvolumes of nfs are exported as individual exports. There are cases where a subdirectory or subdirectories in the volume need to be exported separately. Enabling this option allows any directory on a volumes to be exported separately. Directory exports are enabled by default. + 'nfs.export-dirs' => 'onoff', # default: (on) + + # Enable or disable exporting whole volumes, instead if used in conjunction with nfs3.export-dir, can allow setting up only subdirectories as exports. On by default. + 'nfs.export-volumes' => 'onoff', # default: (on) + + # Use this option to make NFS be faster on systems by using more memory. This option specifies a multiple that determines the total amount of memory used. Default value is 15. Increase to use more memory in order to improve performance for certain use cases. Please consult gluster-users list before using this option. + 'nfs.mem-factor' => 'integer', # default: (null) + + # set the option to 'on' to enable mountd on UDP. Required for some Solaris and AIX NFS clients. The need for enabling this option often depends on the usage of NLM. + 'nfs.mount-udp' => 'offon', # default: (off) + + # This option, if set to 'off', disables NLM server by not registering the service with the portmapper. Set it to 'on' to re-enable it. Default value: 'on' + 'nfs.nlm' => 'onoff', # default: (on) + + # Use this option on systems that need Gluster NFS to be associated with a non-default port number. + 'nfs.port' => 'integer', # default: (null) + + # Allow client connections from unprivileged ports. By default only privileged ports are allowed. Use this option to enable or disable insecure ports for a specific subvolume and to override the global setting set by the previous option. + 'nfs.ports-insecure' => 'offon', # default: (off) + + # For systems that need to run multiple nfs servers, only one registration is possible with portmap service. Use this option to turn off portmap registration for Gluster NFS. On by default + 'nfs.register-with-portmap' => 'onoff', # default: (on) + + # Allow a comma separated list of addresses and/or hostnames to connect to the server. By default, all connections are allowed. This allows users to define a rule for a specific exported volume. + 'nfs.rpc-auth-allow' => 'array', # default: (null) + + # Disable or enable the AUTH_NULL authentication type for a particular exported volume overriding defaults and general setting for AUTH_NULL. Must always be enabled. This option is here only to avoid unrecognized option warnings. + 'nfs.rpc-auth-null' => 'onoff', # default: (on) + + # Reject a comma separated list of addresses and/or hostnames from connecting to the server. By default, all connections are allowed. This allows users to define a rule for a specific exported volume. + 'nfs.rpc-auth-reject' => 'array', # default: (null) + + # Disable or enable the AUTH_UNIX authentication type for a particular exported volume overriding defaults and general setting for AUTH_UNIX scheme. Must always be enabled for better interoperability.However, can be disabled if needed. Enabled by default. + 'nfs.rpc-auth-unix' => 'onoff', # default: (on) + + # Specifies the nfs transport type. Valid transport types are 'tcp' and 'rdma'. + 'nfs.transport-type' => 'string', # default: tcp + + # All writes and COMMIT requests are treated as async. This implies that no write requests are guaranteed to be on server disks when the write reply is received at the NFS client. Trusted sync includes trusted-write behaviour. Off by default. + 'nfs.trusted-sync' => 'offon', # default: (off) + + # On an UNSTABLE write from client, return STABLE flag to force client to not send a COMMIT request. In some environments, combined with a replicated GlusterFS setup, this option can improve write performance. This flag allows user to trust Gluster replication logic to sync data to the disks and recover when required. COMMIT requests if received will be handled in a default manner by fsyncing. STABLE writes are still handled in a sync manner. Off by default. + 'nfs.trusted-write' => 'offon', # default: (off) + + # Type of access desired for this subvolume: read-only, read-write(default) + 'nfs.volume-access' => 'string', # default: (read-write) + + # Maximum file size which would be cached by the io-cache translator. + 'performance.cache-max-file-size' => 'integer', # default: 0 + + # Minimum file size which would be cached by the io-cache translator. + 'performance.cache-min-file-size' => 'integer', # default: 0 + + # Assigns priority to filenames with specific patterns so that when a page needs to be ejected out of the cache, the page of a file whose priority is the lowest will be ejected earlier + 'performance.cache-priority' => 'string', # default: + + # The cached data for a file will be retained till 'cache-refresh-timeout' seconds, after which data re-validation is performed. + 'performance.cache-refresh-timeout' => 'integer', # default: 1 + + # XXX: this appears twice, with different defaults ! + # Size of the read cache. + 'performance.cache-size' => 'string', # default: 32MB + + # Size of the read cache. + 'performance.cache-size' => 'string', # default: 128MB + + # enable/disable io-threads translator in the client graph of volume. + 'performance.client-io-threads' => 'offon', # default: off + + # Enable/Disable least priority + 'performance.enable-least-priority' => 'onoff', # default: on + + # If this option is set ON, instructs write-behind translator to perform flush in background, by returning success (or any errors, if any of previous writes were failed) to application even before flush FOP is sent to backend filesystem. + 'performance.flush-behind' => 'onoff', # default: on + + # Convert all readdir requests to readdirplus to collect stat info on each entry. + 'performance.force-readdirp' => 'onoff', # default: on + + # Max number of threads in IO threads translator which perform high priority IO operations at a given time + 'performance.high-prio-threads' => 'integer', # default: 16 + + # enable/disable io-cache translator in the volume. + 'performance.io-cache' => 'onoff', # default: on + + # Number of threads in IO threads translator which perform concurrent IO operations + 'performance.io-thread-count' => 'integer', # default: 16 + + # Max number of threads in IO threads translator which perform least priority IO operations at a given time + 'performance.least-prio-threads' => 'integer', # default: 1 + + # Max number of least priority operations to handle per-second + 'performance.least-rate-limit' => 'integer', # default: 0 + + # Max number of threads in IO threads translator which perform low priority IO operations at a given time + 'performance.low-prio-threads' => 'integer', # default: 16 + + # Time period after which cache has to be refreshed + 'performance.md-cache-timeout' => 'integer', # default: 1 + + # Max number of threads in IO threads translator which perform normal priority IO operations at a given time + 'performance.normal-prio-threads' => 'integer', # default: 16 + + # enable/disable open-behind translator in the volume. + 'performance.open-behind' => 'onoff', # default: on + + # enable/disable quick-read translator in the volume. + 'performance.quick-read' => 'onoff', # default: on + + # enable/disable read-ahead translator in the volume. + 'performance.read-ahead' => 'onoff', # default: on + + # Number of pages that will be pre-fetched + 'performance.read-ahead-page-count' => 'integer', # default: 4 + + # enable/disable meta-data caching translator in the volume. + 'performance.stat-prefetch' => 'onoff', # default: on + + # This option when set to off, ignores the O_DIRECT flag. + 'performance.strict-o-direct' => 'offon', # default: off + + # Do not let later writes overtake earlier writes even if they do not overlap + 'performance.strict-write-ordering' => 'offon', # default: off + + # enable/disable write-behind translator in the volume. + 'performance.write-behind' => 'onoff', # default: on + + # Size of the write-behind buffer for a single file (inode). + 'performance.write-behind-window-size' => 'string', # default: 1MB + + # NOTE: this option taken from gluster documentation + # Allow client connections from unprivileged ports. By default only privileged ports are allowed. This is a global setting in case insecure ports are to be enabled for all exports using a single option. + 'server.allow-insecure' => 'offon', # XXX: description and manual differ in their mention of what the correct default is, so which is it? + + # Map requests from uid/gid 0 to the anonymous uid/gid. Note that this does not apply to any other uids or gids that might be equally sensitive, such as user bin or group staff. + 'server.root-squash' => 'offon', # default: off + + # Specifies directory in which gluster should save its statedumps. By default it is the /tmp directory + 'server.statedump-path' => 'string', # default: /var/run/gluster + + # Support for native Linux AIO + 'storage.linux-aio' => 'offon', # default: off + + # Support for setting gid of brick's owner + 'storage.owner-gid' => 'integer', # default: (null) + + # Support for setting uid of brick's owner + 'storage.owner-uid' => 'integer', # default: (null) + } + + # join char + $jchars = { + 'auth.allow' => ',', + 'auth.reject' => ',', + 'nfs.rpc-auth-allow' => ',', + 'nfs.rpc-auth-reject' => ',', + } +} + +# vim: ts=8 diff --git a/gluster/manifests/volume/property/group.pp b/gluster/manifests/volume/property/group.pp new file mode 100644 index 000000000..d0b7a60fe --- /dev/null +++ b/gluster/manifests/volume/property/group.pp @@ -0,0 +1,73 @@ +# GlusterFS module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: The most well known group is 'virt', and is a collection of properties. +# NOTE: This type is particularly useful, because if you've set a certain group +# for your volume, and your package updates the group properties, then this can +# notice those changes and keep your volume in sync with the latest properties! +# NOTE: This intentionally conflicts with properties that are defined manually. +# NOTE: this does the equivalent of: gluster volume set group + +define gluster::volume::property::group( + $vip = '', # vip of the cluster (optional but recommended) +) { + include gluster::xml + include gluster::vardir + include gluster::volume::property::group::data + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + $split = split($name, '#') # do some $name parsing + $volume = $split[0] # volume name + $group = $split[1] # group name + + if ! ( "${volume}#${group}" == "${name}" ) { + fail('The property $name must match a $volume#$group pattern.') + } + + $groups = split($gluster_property_groups, ',') # fact + + if ! ("${group}" in $groups) { + + # check a fact to see if the directory is built yet... this + # prevents weird corner cases where this module is added to + # a new machine which is already built, except doesn't have + # the custom group data installed yet. if we fail, we won't + # be able to install it, so we don't fail, we warn instead! + if "${gluster_property_groups_ready}" == 'true' { + warning("The group named '${group}' is not available.") + } else { + notice("The group '${group}' might not be built yet.") + } + } else { + # read the fact that comes from the data in: /var/lib/glusterd/groups/* + $group_data_string = getvar("gluster_property_group_${group}") # fact! + # each element in this list is a key=value string + $group_data_list = split("${group_data_string}", ',') + # split into the correct hash to create all the properties + $group_data_yaml = inline_template("<%= @group_data_list.inject(Hash.new) { |h,i| { '${volume}#'+((i.split('=').length == 2) ? i.split('=')[0] : '') => {'value' => ((i.split('=').length == 2) ? i.split('=')[1] : '')} }.merge(h) }.to_yaml %>") + # build into a hash + $group_data_hash = parseyaml($group_data_yaml) + # pass through the vip + $group_data_defaults = {'vip' => "${vip}"} + # create the properties + create_resources('gluster::volume::property', $group_data_hash, $group_data_defaults) + } +} + +# vim: ts=8 diff --git a/gluster/manifests/volume/property/group/data.pp b/gluster/manifests/volume/property/group/data.pp new file mode 100644 index 000000000..b79c4dd0a --- /dev/null +++ b/gluster/manifests/volume/property/group/data.pp @@ -0,0 +1,43 @@ +# GlusterFS module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this provides an internal repository of volume parameter set groups and +# can be useful if the version of glusterfs does not have set group support, or +# if this module wants to distribute some custom groups which are not upstream. + +class gluster::volume::property::group::data() { + + include gluster::vardir + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + file { "${vardir}/groups/": + source => 'puppet:///modules/gluster/groups/', + ensure => directory, + recurse => true, + purge => true, + force => true, + owner => root, + group => nobody, + mode => 644, # u=rwx + backup => false, # don't backup to filebucket + require => File["${vardir}/"], + } +} + +# vim: ts=8 diff --git a/gluster/manifests/wrapper.pp b/gluster/manifests/wrapper.pp new file mode 100644 index 000000000..54156de33 --- /dev/null +++ b/gluster/manifests/wrapper.pp @@ -0,0 +1,135 @@ +# GlusterFS module by James +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::wrapper( + $nodetree, + $volumetree, + $vip = '', # vip of the cluster (optional but recommended) + + $nfs = false, # TODO in server.pp + $shorewall = false, + $zone = 'net', # TODO: allow a list of zones + $allow = 'all' +) { + # + # build gluster::server + # + + $hosts = split(inline_template("<%= @nodetree.keys.join(',') %>"), ',') + $ips = split(inline_template('<%= @nodetree.map{ |host,value| \'#{value["ip"]}\' }.join(",") %>'), ',') + + class { 'gluster::server': + hosts => $hosts, + ips => $ips, +#XXX: TODO? clients => XXX, + nfs => $nfs, + shorewall => $shorewall, + zone => $zone, + allow => $allow, + } + + # + # build gluster::host + # + + # EXAMPLE: + #gluster::host { 'annex1.example.com': + # # use uuidgen to make these + # uuid => '1f660ca2-2c78-4aa0-8f4d-21608218c69c', + #} + + # filter the nodetree so that only host elements with uuid's are left + # XXX: each_with_object doesn't exist in rhel6 ruby, so use inject + #$hosttree = inline_template('<%= @nodetree.each_with_object({}) {|(x,y), h| h[x] = y.select{ |key,value| ["uuid"].include?(key) } }.to_yaml %>') + $hosttree = inline_template('<%= @nodetree.inject({}) {|h, (x,y)| h[x] = y.select{ |key,value| ["uuid"].include?(key) }; h }.to_yaml %>') + # newhash = oldhash.inject({}) { |h,(k,v)| h[k] = some_operation(v); h } # XXX: does this form work ? + $yaml_host = parseyaml($hosttree) + create_resources('gluster::host', $yaml_host) + + # + # build gluster::brick + # + + # EXAMPLE: + #gluster::brick { 'annex1.example.com:/mnt/storage1a': + # dev => '/dev/disk/by-id/scsi-36003048007e26c00173ad3b633a2ef67', # /dev/sda + # labeltype => 'gpt', + # fstype => 'xfs', + # fsuuid => '1ae49642-7f34-4886-8d23-685d13867fb1', + # xfs_inode64 => true, + # xfs_nobarrier => true, + # areyousure => true, + #} + + # filter the nodetree and build out each brick element from the hosts + $bricktree = inline_template('<%= r = {}; @nodetree.each {|x,y| y["bricks"].each {|k,v| r[x+":"+k] = v} }; r.to_yaml %>') + # this version removes any invalid keys from the brick specifications + #$bricktree = inline_template('<%= r = {}; @nodetree.each {|x,y| y["bricks"].each {|k,v| r[x+":"+k] = v.select{ |key,value| ["dev", "labeltype", "fstype", "fsuuid", "..."].include?(key) } } }; r.to_yaml %>') + $yaml_brick = parseyaml($bricktree) + create_resources('gluster::brick', $yaml_brick) + + # + # build gluster::volume + # + + # EXAMPLE: + #gluster::volume { 'examplevol': + # replica => 2, + # bricks => $brick_list, + # start => undef, # i'll start this myself + #} + + # to be used as default gluster::volume brick list + $bricklist = split(inline_template("<%= @bricktree.keys.join(',') %>"), ',') + + # semi ok method: + #$volumetree_defaults_all = { + # "bricks" => $bricklist, + # "transport" => 'tcp', + # "replica" => 1, + # "stripe" => 1, + # "vip" => $vip, + # "start" => undef, # ? + #} + #$semi_ok = inline_template('<%= @volumetree.each_with_object({}) {|(x,y), h| h[x] = @volumetree_defaults_all.each_with_object({}) {|(xx,yy), hh| hh[xx] = y.fetch(xx, @volumetree_defaults_all[xx])} }.to_yaml %>') + + # good method + $volumetree_defaults = { + 'bricks' => $bricklist, + 'vip' => $vip, + } + # loop through volumetree... if special defaults are missing, then add! + $volumetree_updated = inline_template('<%= @volumetree.each_with_object({}) {|(x,y), h| h[x] = y; @volumetree_defaults.each {|k,v| h[k] = h.fetch(k, v)} }.to_yaml %>') + $yaml_volume = parseyaml($volumetree_updated) + create_resources('gluster::volume', $yaml_volume) + + # + # build gluster::volume::property (auth.allow) + # + + # EXAMPLE: + #gluster::volume::property { 'examplevol#auth.allow': + # value => '192.0.2.13,198.51.100.42,203.0.113.69', + #} + + #$simplewrongname = inline_template('<%= @volumetree.each_with_object({}) {|(x,y), h| h[x+"#auth.allow"] = y.select{ |key,value| ["clients"].include?(key) } }.to_yaml %>') + $propertytree = inline_template('<%= @volumetree.each_with_object({}) {|(x,y), h| h[x+"#auth.allow"] = { "value" => y.fetch("clients", []) } }.to_yaml %>') + $yaml_volume_property = parseyaml($propertytree) + create_resources('gluster::volume::property', $yaml_volume_property) +} + +# vim: ts=8 diff --git a/gluster/manifests/xml.pp b/gluster/manifests/xml.pp new file mode 100644 index 000000000..489a7c5d4 --- /dev/null +++ b/gluster/manifests/xml.pp @@ -0,0 +1,50 @@ +# GlusterFS module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class gluster::xml { + include gluster::vardir + include gluster::params + + #$vardir = $::gluster::vardir::module_vardir # with trailing slash + $vardir = regsubst($::gluster::vardir::module_vardir, '\/$', '') + + # argparse is built into python on new platforms and isn't needed here! + if "${::gluster::params::package_python_argparse}" != '' { + package { "${::gluster::params::package_python_argparse}": + ensure => present, + before => File["${vardir}/xml.py"], + } + } + + # for parsing gluster xml output + package { "${::gluster::params::package_python_lxml}": + ensure => present, + before => File["${vardir}/xml.py"], + } + + file { "${vardir}/xml.py": + source => 'puppet:///modules/gluster/xml.py', + owner => root, + group => nobody, + mode => 700, # u=rwx + backup => false, # don't backup to filebucket + ensure => present, + require => File["${vardir}/"], + } +} + +# vim: ts=8 diff --git a/gluster/puppet-gluster-documentation.pdf b/gluster/puppet-gluster-documentation.pdf new file mode 100644 index 000000000..69ba9de03 Binary files /dev/null and b/gluster/puppet-gluster-documentation.pdf differ diff --git a/gluster/puppet-gluster.spec.in b/gluster/puppet-gluster.spec.in new file mode 100644 index 000000000..0fde079fd --- /dev/null +++ b/gluster/puppet-gluster.spec.in @@ -0,0 +1,49 @@ +# special thanks to kkeithley for using his wizard rpm skills to get this going +%global puppet_module_version __VERSION__ + +Name: puppet-gluster +Version: __VERSION__ +#Release: __RELEASE__%{?dist} # use this to make dist specific builds +Release: __RELEASE__ +Summary: A puppet module for GlusterFS +License: AGPLv3+ +URL: https://github.com/purpleidea/puppet-gluster +Source0: https://download.gluster.org/pub/gluster/purpleidea/puppet-gluster/SOURCES/puppet-gluster-%{puppet_module_version}.tar.bz2 +BuildArch: noarch + +Requires: puppet >= 3.0.0 +Requires: puppetlabs-stdlib >= 4.1.0 + +# these should be 'Suggests:' or similar, since they aren't absolutely required +#Requires: puppetlabs-apt >= 1.4.0 +Requires: puppet-common >= 0.0.1 +Requires: puppet-keepalived >= 0.0.1 +Requires: puppet-puppet >= 0.0.1 +Requires: puppet-shorewall >= 0.0.1 +Requires: puppet-yum >= 0.0.1 + +%description +A puppet module used for installing, configuring and managing GlusterFS. + +%prep +%setup -c -q -T -D -a 0 + +find %{_builddir} -type f -name ".*" -exec rm {} + +find %{_builddir} -size 0 -exec rm {} + +find %{_builddir} \( -name "*.pl" -o -name "*.sh" \) -exec chmod +x {} + +find %{_builddir} \( -name "*.pp" -o -name "*.py" \) -exec chmod -x {} + +find %{_builddir} \( -name "*.rb" -o -name "*.erb" \) -exec chmod -x {} + -exec sed -i "/^#!/{d;q}" {} + + +%build + +%install +rm -rf %{buildroot} +# _datadir is typically /usr/share/ +install -d -m 0755 %{buildroot}/%{_datadir}/puppet/modules/ +cp -r puppet-gluster-%{puppet_module_version} %{buildroot}/%{_datadir}/puppet/modules/gluster + +%files +%{_datadir}/puppet/modules/* + +# this changelog is auto-generated by git log +%changelog diff --git a/gluster/templates/glusterd.info.erb b/gluster/templates/glusterd.info.erb new file mode 100644 index 000000000..94acebc9e --- /dev/null +++ b/gluster/templates/glusterd.info.erb @@ -0,0 +1,4 @@ +UUID=<%= @valid_uuid %> +<% if @operating_version != '' and @operating_version != '-1' -%> +operating-version=<%= @operating_version %> +<% end -%> diff --git a/gluster/templates/glusterd.vol.erb b/gluster/templates/glusterd.vol.erb new file mode 100644 index 000000000..a258b9a4a --- /dev/null +++ b/gluster/templates/glusterd.vol.erb @@ -0,0 +1,16 @@ +volume management + type mgmt/glusterd + option working-directory /var/lib/glusterd + option transport-type socket,rdma + option transport.socket.keepalive-time 10 + option transport.socket.keepalive-interval 2 + option transport.socket.read-fail-log off +<% if @valid_rpcauthallowinsecure -%> + option rpc-auth-allow-insecure on +<% end -%> +<% if @valid_baseport.to_i > 0 -%> + option base-port <%= @valid_baseport %> +<% else -%> +# option base-port 49152 +<% end -%> +end-volume diff --git a/gluster/vagrant/.gitignore b/gluster/vagrant/.gitignore new file mode 100644 index 000000000..3883822f5 --- /dev/null +++ b/gluster/vagrant/.gitignore @@ -0,0 +1,3 @@ +puppet-gluster.yaml +.vagrant/ +.ssh/ diff --git a/gluster/vagrant/README b/gluster/vagrant/README new file mode 100644 index 000000000..7918d1160 --- /dev/null +++ b/gluster/vagrant/README @@ -0,0 +1,31 @@ +This is Puppet-Gluster+Vagrant! (https://ttboj.wordpress.com/) + +You'll first need to get vagrant working. Here are some background articles: + +* https://ttboj.wordpress.com/2013/12/09/vagrant-on-fedora-with-libvirt/ +* https://ttboj.wordpress.com/2013/12/21/vagrant-vsftp-and-other-tricks/ +* https://ttboj.wordpress.com/2014/01/02/vagrant-clustered-ssh-and-screen/ + +I've written a detailed article about all of this, which is available here: + +* https://ttboj.wordpress.com/2014/01/08/automatically-deploying-glusterfs-with-puppet-gluster-vagrant + +This will not work perfectly on Fedora 19. You must use Fedora 20 or greater. + +Once you're comfortable that vagrant is working properly, run this command: + + vagrant up puppet && sudo -v && vagrant up annex{1..4} --no-parallel + +Sit back and watch, or go have a beverage... +The first run can take a while because it has to download/install a base image. + +When the above command completes, puppet will still be provisioning your hosts. +Once the cluster state settles, puppet will create and start a gluster volume. +Once your volume is started, you can build a few clients: + + vagrant up client{1..2} --gluster-clients=2 + +Happy hacking, + +James + diff --git a/gluster/vagrant/Vagrantfile b/gluster/vagrant/Vagrantfile new file mode 100644 index 000000000..aef25d1bb --- /dev/null +++ b/gluster/vagrant/Vagrantfile @@ -0,0 +1,599 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Vagrantfile for GlusterFS using Puppet-Gluster +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: vagrant-libvirt needs to run in series (not in parallel) to avoid trying +# to create the network twice, eg: 'vagrant up --no-parallel'. alternatively you +# can build the first host (puppet server) manually, and then the hosts next eg: +# 'vagrant up puppet && vagrant up' which ensures the puppet server is up first! +# NOTE: https://github.com/pradels/vagrant-libvirt/issues/104 is the open bug... + +# README: https://ttboj.wordpress.com/2013/12/09/vagrant-on-fedora-with-libvirt/ +# README: https://ttboj.wordpress.com/2013/12/21/vagrant-vsftp-and-other-tricks/ +# ALSO: https://ttboj.wordpress.com/2014/01/02/vagrant-clustered-ssh-and-screen/ +# README: https://ttboj.wordpress.com/2014/01/08/automatically-deploying-glusterfs-with-puppet-gluster-vagrant + +# NOTE: this will not work properly on Fedora 19 or anything that does not have +# the libvirt broadcast patch included. You can check which libvirt version you +# have and see if that version tag is in the libvirt git or in your distro. eg: +# git tag --contains 51e184e9821c3740ac9b52055860d683f27b0ab6 | grep +# this is because old libvirt broke the vrrp broadcast packets from keepalived! + +# TODO: the /etc/hosts DNS setup is less than ideal, but I didn't implement +# anything better yet. Please feel free to suggest something else! + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = '2' + +require 'ipaddr' +require 'yaml' + +# +# globals +# +domain = 'example.com' +network = IPAddr.new '192.168.142.0/24' +range = network.to_range.to_a +cidr = (32-(Math.log(range.length)/Math.log(2))).to_i +offset = 100 # start gluster hosts after here +#puts range[0].to_s # network +#puts range[1].to_s # router (reserved) +#puts range[2].to_s # puppetmaster +#puts range[3].to_s # vip + +network2 = IPAddr.new '192.168.143.0/24' +range2 = network2.to_range.to_a +cidr2 = (32-(Math.log(range2.length)/Math.log(2))).to_i +offset2 = 2 +netmask2 = IPAddr.new('255.255.255.255').mask(cidr2).to_s + +# mutable by ARGV and settings file +count = 4 # default number of gluster hosts to build +disks = 0 # default number of disks to attach (after the host os) +bricks = 0 # default number of bricks to build (0 defaults to 1) +fstype = '' # default filesystem for bricks (requires bricks > 0) +version = '' # default gluster version (empty string means latest!) +firewall = false # default firewall enabled (FIXME: default to true when keepalived bug is fixed) +replica = 1 # default replica count +clients = 1 # default number of gluster clients to build +layout = '' # default brick layout +setgroup = '' # default setgroup value +sync = 'rsync' # default sync type +cachier = false # default cachier usage + +# +# ARGV parsing +# +projectdir = File.expand_path File.dirname(__FILE__) # vagrant project dir!! +f = File.join(projectdir, 'puppet-gluster.yaml') + +# load settings +if File.exist?(f) + settings = YAML::load_file f + count = settings[:count] + disks = settings[:disks] + bricks = settings[:bricks] + fstype = settings[:fstype] + version = settings[:version] + firewall = settings[:firewall] + replica = settings[:replica] + clients = settings[:clients] + layout = settings[:layout] + setgroup = settings[:setgroup] + sync = settings[:sync] + cachier = settings[:cachier] +end + +# ARGV parser +skip = 0 +while skip < ARGV.length + #puts "#{skip}, #{ARGV[skip]}" # debug + if ARGV[skip].start_with?(arg='--gluster-count=') + v = ARGV.delete_at(skip).dup + v.slice! arg + #puts "#{arg}, #{v}" # debug + + count = v.to_i # set gluster host count + + elsif ARGV[skip].start_with?(arg='--gluster-disks=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + disks = v.to_i # set gluster disk count + + elsif ARGV[skip].start_with?(arg='--gluster-bricks=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + bricks = v.to_i # set gluster brick count + + elsif ARGV[skip].start_with?(arg='--gluster-fstype=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + fstype = v.to_s # set gluster fstype + + elsif ARGV[skip].start_with?(arg='--gluster-version=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + version = v.to_s # set gluster version + + elsif ARGV[skip].start_with?(arg='--gluster-firewall=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + firewall = v.to_s # set firewall flag + if ['false', 'no'].include?(firewall.downcase) + firewall = false + else + firewall = true + end + + elsif ARGV[skip].start_with?(arg='--gluster-replica=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + replica = v.to_i # set gluster replica + + elsif ARGV[skip].start_with?(arg='--gluster-clients=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + clients = v.to_i # set gluster client count + + elsif ARGV[skip].start_with?(arg='--gluster-layout=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + layout = v.to_s # set gluster brick layout + + elsif ARGV[skip].start_with?(arg='--gluster-setgroup=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + setgroup = v.to_s # set gluster setgroup + + elsif ARGV[skip].start_with?(arg='--gluster-sync=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + sync = v.to_s # set sync type + + elsif ARGV[skip].start_with?(arg='--gluster-cachier=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + cachier = v.to_s # set cachier flag + if ['true', 'yes'].include?(cachier.downcase) + cachier = true + else + cachier = false + end + + else # skip over "official" vagrant args + skip = skip + 1 + end +end + +# save settings (ARGV overrides) +settings = { + :count => count, + :disks => disks, + :bricks => bricks, + :fstype => fstype, + :version => version, + :firewall => firewall, + :replica => replica, + :clients => clients, + :layout => layout, + :setgroup => setgroup, + :sync => sync, + :cachier => cachier +} +File.open(f, 'w') do |file| + file.write settings.to_yaml +end + +#puts "ARGV: #{ARGV}" # debug + +# erase host information from puppet so that the user can do partial rebuilds +snoop = ARGV.select { |x| !x.start_with?('-') } +if snoop.length > 1 and snoop[0] == 'destroy' + snoop.shift # left over array snoop should be list of hosts + if snoop.include?('puppet') # doesn't matter then... + snoop = [] + end +else + # important! clear snoop because we're not using 'destroy' + snoop = [] +end + +# figure out which hosts are getting destroyed +destroy = ARGV.select { |x| !x.start_with?('-') } +if destroy.length > 0 and destroy[0] == 'destroy' + destroy.shift # left over array destroy should be list of hosts or [] + if destroy.length == 0 + destroy = true # destroy everything + end +else + destroy = false # destroy nothing +end + +# figure out which hosts are getting provisioned +provision = ARGV.select { |x| !x.start_with?('-') } +if provision.length > 0 and ['up', 'provision'].include?(provision[0]) + provision.shift # left over array provision should be list of hosts or [] + if provision.length == 0 + provision = true # provision everything + end +else + provision = false # provision nothing +end + +# XXX: workaround for: https://github.com/mitchellh/vagrant/issues/2447 +# only run on 'vagrant init' or if it's the first time running vagrant +if sync == 'nfs' and ((ARGV.length > 0 and ARGV[0] == 'init') or not(File.exist?(f))) + `sudo systemctl restart nfs-server` + `firewall-cmd --permanent --zone public --add-service mountd` + `firewall-cmd --permanent --zone public --add-service rpc-bind` + `firewall-cmd --permanent --zone public --add-service nfs` + `firewall-cmd --reload` +end + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + + #config.landrush.enable # TODO ? + + # + # box (pre-built base image) + # + config.vm.box = 'centos-6' # i built it + + # box source url + # TODO: this box should be GPG signed + config.vm.box_url = 'https://download.gluster.org/pub/gluster/purpleidea/vagrant/centos-6/centos-6.box' + + # + # sync type + # + config.vm.synced_folder './', '/vagrant', type: sync # nfs, rsync + + # + # cache + # + # NOTE: you should probably erase the cache between rebuilds if you are + # installing older package versions. This is because the newer packages + # will get cached, and then subsequently might get silently installed!! + if cachier + # TODO: this doesn't cache metadata, full offline operation not possible + config.cache.auto_detect = true + config.cache.enable :yum + #config.cache.enable :apt + if not ARGV.include?('--no-parallel') # when running in parallel, + config.cache.scope = :machine # use the per machine cache + end + if sync == 'nfs' # TODO: support other sync types here... + config.cache.enable_nfs = true # sets nfs => true on the synced_folder + # the nolock option is required, otherwise the NFSv3 client will try to + # access the NLM sideband protocol to lock files needed for /var/cache/ + # all of this can be avoided by using NFSv4 everywhere. die NFSv3, die! + config.cache.mount_options = ['rw', 'vers=3', 'tcp', 'nolock'] + end + end + + # + # vip + # + vip_ip = range[3].to_s + vip_hostname = 'annex' + + # + # puppetmaster + # + puppet_ip = range[2].to_s + puppet_hostname = 'puppet' + fv = File.join(projectdir, '.vagrant', "#{puppet_hostname}-hosts.done") + if destroy.is_a?(TrueClass) or (destroy.is_a?(Array) and destroy.include?(puppet_hostname)) + if File.exists?(fv) # safety + puts "Unlocking shell provisioning for: #{puppet_hostname}..." + File.delete(fv) # delete hosts token + end + end + + #puppet_fqdn = "#{puppet_hostname}.#{domain}" + config.vm.define :puppet, :primary => true do |vm| + vm.vm.hostname = puppet_hostname + # red herring network so that management happens here... + vm.vm.network :private_network, + :ip => range2[2].to_s, + :libvirt__netmask => netmask2, + #:libvirt__dhcp_enabled => false, # XXX: not allowed here + :libvirt__network_name => 'default' + + # this is the real network that we'll use... + vm.vm.network :private_network, + :ip => puppet_ip, + :libvirt__dhcp_enabled => false, + :libvirt__network_name => 'gluster' + + #vm.landrush.host puppet_hostname, puppet_ip # TODO ? + + # ensure the gluster module is present for provisioning... + if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(puppet_hostname)) + cwd = `pwd` + mod = File.join(projectdir, 'puppet', 'modules') + `cd #{mod} && make gluster &> /dev/null; cd #{cwd}` + end + + # + # shell + # + if not File.exists?(fv) # only modify /etc/hosts once + if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(puppet_hostname)) + File.open(fv, 'w') {} # touch + end + vm.vm.provision 'shell', inline: 'puppet resource host localhost.localdomain ip=127.0.0.1 host_aliases=localhost' + vm.vm.provision 'shell', inline: "puppet resource host #{puppet_hostname} ensure=absent" # so that fqdn works + + vm.vm.provision 'shell', inline: "puppet resource host #{vip_hostname}.#{domain} ip=#{vip_ip} host_aliases=#{vip_hostname} ensure=present" + vm.vm.provision 'shell', inline: "puppet resource host #{puppet_hostname}.#{domain} ip=#{puppet_ip} host_aliases=#{puppet_hostname} ensure=present" + (1..count).each do |i| + h = "annex#{i}" + ip = range[offset+i].to_s + vm.vm.provision 'shell', inline: "puppet resource host #{h}.#{domain} ip=#{ip} host_aliases=#{h} ensure=present" + end + + # hosts entries for all clients + (1..clients).each do |i| + h = "client#{i}" + ip = range[offset+count+i].to_s + vm.vm.provision 'shell', inline: "puppet resource host #{h}.#{domain} ip=#{ip} host_aliases=#{h} ensure=present" + end + end + # + # puppet (apply) + # + vm.vm.provision :puppet do |puppet| + puppet.module_path = 'puppet/modules' + puppet.manifests_path = 'puppet/manifests' + puppet.manifest_file = 'site.pp' + # custom fact + puppet.facter = { + 'vagrant' => '1', + 'vagrant_gluster_firewall' => firewall ? 'true' : 'false', + 'vagrant_gluster_allow' => (1..count).map{|z| range[offset+z].to_s}.join(','), + } + puppet.synced_folder_type = sync + end + + vm.vm.provider :libvirt do |libvirt| + libvirt.cpus = 2 + libvirt.memory = 1024 + end + end + + # + # annex + # + (1..count).each do |i| + h = "annex#{i}" + ip = range[offset+i].to_s # eg: "192.168.142.#{100+i}" + #fqdn = "annex#{i}.#{domain}" + fvx = File.join(projectdir, '.vagrant', "#{h}-hosts.done") + if destroy.is_a?(TrueClass) or (destroy.is_a?(Array) and destroy.include?(h)) + if File.exists?(fvx) # safety + puts "Unlocking shell provisioning for: #{h}..." + File.delete(fvx) # delete hosts token + end + end + + if snoop.include?(h) # should we clean this machine? + cmd = "puppet cert clean #{h}.#{domain}" + puts "Running 'puppet cert clean' for: #{h}..." + `vagrant ssh #{puppet_hostname} -c 'sudo #{cmd}'` + cmd = "puppet node deactivate #{h}.#{domain}" + puts "Running 'puppet node deactivate' for: #{h}..." + `vagrant ssh #{puppet_hostname} -c 'sudo #{cmd}'` + end + + config.vm.define h.to_sym do |vm| + vm.vm.hostname = h + # red herring network so that management happens here... + vm.vm.network :private_network, + :ip => range2[offset2+i].to_s, + :libvirt__netmask => netmask2, + :libvirt__network_name => 'default' + + # this is the real network that we'll use... + vm.vm.network :private_network, + :ip => ip, + :libvirt__dhcp_enabled => false, + :libvirt__network_name => 'gluster' + + #vm.landrush.host h, ip # TODO ? + + # + # shell + # + if not File.exists?(fvx) # only modify /etc/hosts once + if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(h)) + File.open(fvx, 'w') {} # touch + end + vm.vm.provision 'shell', inline: 'puppet resource host localhost.localdomain ip=127.0.0.1 host_aliases=localhost' + vm.vm.provision 'shell', inline: "puppet resource host #{h} ensure=absent" # so that fqdn works + + vm.vm.provision 'shell', inline: "puppet resource host #{vip_hostname}.#{domain} ip=#{vip_ip} host_aliases=#{vip_hostname} ensure=present" + vm.vm.provision 'shell', inline: "puppet resource host #{puppet_hostname}.#{domain} ip=#{puppet_ip} host_aliases=#{puppet_hostname} ensure=present" + #vm.vm.provision 'shell', inline: "[ ! -e /root/puppet-cert-is-clean ] && ssh -o 'StrictHostKeyChecking=no' #{puppet_hostname} puppet cert clean #{h}.#{domain} ; touch /root/puppet-cert-is-clean" + # hosts entries for all hosts + (1..count).each do |j| + oh = "annex#{j}" + oip = range[offset+j].to_s # eg: "192.168.142.#{100+i}" + vm.vm.provision 'shell', inline: "puppet resource host #{oh}.#{domain} ip=#{oip} host_aliases=#{oh} ensure=present" + end + + # hosts entries for all clients + (1..clients).each do |j| + oh = "client#{j}" + oip = range[offset+count+j].to_s + vm.vm.provision 'shell', inline: "puppet resource host #{oh}.#{domain} ip=#{oip} host_aliases=#{oh} ensure=present" + end + end + # + # puppet (agent) + # + vm.vm.provision :puppet_server do |puppet| + #puppet.puppet_node = "#{h}" # redundant + #puppet.puppet_server = "#{puppet_hostname}.#{domain}" + puppet.puppet_server = puppet_hostname + #puppet.options = '--verbose --debug' + puppet.options = '--test' # see the output + puppet.facter = { + 'vagrant' => '1', + 'vagrant_gluster_disks' => disks.to_s, + 'vagrant_gluster_bricks' => bricks.to_s, + 'vagrant_gluster_fstype' => fstype.to_s, + 'vagrant_gluster_replica' => replica.to_s, + 'vagrant_gluster_layout' => layout, + 'vagrant_gluster_setgroup' => setgroup, + 'vagrant_gluster_vip' => vip_ip, + 'vagrant_gluster_vip_fqdn' => "#{vip_hostname}.#{domain}", + 'vagrant_gluster_firewall' => firewall ? 'true' : 'false', + 'vagrant_gluster_version' => version, + } + end + + vm.vm.provider :libvirt do |libvirt| + # add additional disks to the os + (1..disks).each do |j| # if disks is 0, this passes :) + #print "disk: #{j}" + libvirt.storage :file, + #:path => '', # auto! + #:device => 'vdb', # auto! + #:size => '10G', # auto! + :type => 'qcow2' + + end + end + end + end + + # + # client + # + (1..clients).each do |i| + h = "client#{i}" + ip = range[offset+count+i].to_s + #fqdn = "annex#{i}.#{domain}" + fvy = File.join(projectdir, '.vagrant', "#{h}-hosts.done") + if destroy.is_a?(TrueClass) or (destroy.is_a?(Array) and destroy.include?(h)) + if File.exists?(fvy) # safety + puts "Unlocking shell provisioning for: #{h}..." + File.delete(fvy) # delete hosts token + end + end + + if snoop.include?(h) # should we clean this machine? + cmd = "puppet cert clean #{h}.#{domain}" + puts "Running 'puppet cert clean' for: #{h}..." + `vagrant ssh #{puppet_hostname} -c 'sudo #{cmd}'` + cmd = "puppet node deactivate #{h}.#{domain}" + puts "Running 'puppet node deactivate' for: #{h}..." + `vagrant ssh #{puppet_hostname} -c 'sudo #{cmd}'` + end + + config.vm.define h.to_sym do |vm| + vm.vm.hostname = h + # red herring network so that management happens here... + vm.vm.network :private_network, + :ip => range2[offset2+count+i].to_s, + :libvirt__netmask => netmask2, + :libvirt__network_name => 'default' + + # this is the real network that we'll use... + vm.vm.network :private_network, + :ip => ip, + :libvirt__dhcp_enabled => false, + :libvirt__network_name => 'gluster' + + #vm.landrush.host h, ip # TODO ? + + # + # shell + # + if not File.exists?(fvy) # only modify /etc/hosts once + if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(h)) + File.open(fvy, 'w') {} # touch + end + vm.vm.provision 'shell', inline: 'puppet resource host localhost.localdomain ip=127.0.0.1 host_aliases=localhost' + vm.vm.provision 'shell', inline: "puppet resource host #{h} ensure=absent" # so that fqdn works + + vm.vm.provision 'shell', inline: "puppet resource host #{vip_hostname}.#{domain} ip=#{vip_ip} host_aliases=#{vip_hostname} ensure=present" + vm.vm.provision 'shell', inline: "puppet resource host #{puppet_hostname}.#{domain} ip=#{puppet_ip} host_aliases=#{puppet_hostname} ensure=present" + # hosts entries for all hosts + (1..count).each do |j| + oh = "annex#{j}" + oip = range[offset+j].to_s # eg: "192.168.142.#{100+i}" + vm.vm.provision 'shell', inline: "puppet resource host #{oh}.#{domain} ip=#{oip} host_aliases=#{oh} ensure=present" + end + + # hosts entries for all clients + (1..clients).each do |j| + oh = "client#{j}" + oip = range[offset+count+j].to_s + vm.vm.provision 'shell', inline: "puppet resource host #{oh}.#{domain} ip=#{oip} host_aliases=#{oh} ensure=present" + end + end + # + # puppet (agent) + # + vm.vm.provision :puppet_server do |puppet| + #puppet.puppet_node = "#{h}" # redundant + #puppet.puppet_server = "#{puppet_hostname}.#{domain}" + puppet.puppet_server = puppet_hostname + #puppet.options = '--verbose --debug' + puppet.options = '--test' # see the output + puppet.facter = { + 'vagrant' => '1', + 'vagrant_gluster_vip' => vip_ip, + 'vagrant_gluster_vip_fqdn' => "#{vip_hostname}.#{domain}", + 'vagrant_gluster_firewall' => firewall ? 'true' : 'false', + 'vagrant_gluster_version' => version, + } + end + end + end + + # + # libvirt + # + config.vm.provider :libvirt do |libvirt| + libvirt.driver = 'kvm' # needed for kvm performance benefits ! + # leave out to connect directly with qemu:///system + #libvirt.host = 'localhost' + libvirt.connect_via_ssh = false + libvirt.username = 'root' + libvirt.storage_pool_name = 'default' + #libvirt.default_network = 'default' # XXX: this does nothing + libvirt.default_prefix = 'gluster' # prefix for your vm's! + end + +end + diff --git a/gluster/vagrant/puppet/files/README b/gluster/vagrant/puppet/files/README new file mode 100644 index 000000000..3f5a63aa7 --- /dev/null +++ b/gluster/vagrant/puppet/files/README @@ -0,0 +1,2 @@ +This is Puppet-Gluster+Vagrant! (https://ttboj.wordpress.com/) + diff --git a/gluster/vagrant/puppet/hiera.yaml b/gluster/vagrant/puppet/hiera.yaml new file mode 100644 index 000000000..5aaf25d18 --- /dev/null +++ b/gluster/vagrant/puppet/hiera.yaml @@ -0,0 +1,7 @@ +--- +:backends: + - yaml +:yaml: + :datadir: /etc/puppet/hieradata/ +:hierarchy: + - common diff --git a/gluster/vagrant/puppet/hieradata/common.yaml b/gluster/vagrant/puppet/hieradata/common.yaml new file mode 100644 index 000000000..d0fa00580 --- /dev/null +++ b/gluster/vagrant/puppet/hieradata/common.yaml @@ -0,0 +1,3 @@ +--- +welcome: 'This is Puppet-Gluster+Vagrant! (https://ttboj.wordpress.com/)' +# vim:expandtab ts=8 sw=8 sta diff --git a/gluster/vagrant/puppet/manifests/site.pp b/gluster/vagrant/puppet/manifests/site.pp new file mode 100644 index 000000000..e0decc3ad --- /dev/null +++ b/gluster/vagrant/puppet/manifests/site.pp @@ -0,0 +1,197 @@ +node default { + # this will get put on every host... + $url = 'https://ttboj.wordpress.com/' + file { '/etc/motd': + content => "This is Puppet-Gluster+Vagrant! (${url})\n", + } +} + +# puppetmaster +node puppet inherits default { + + if "${::vagrant_gluster_firewall}" != 'false' { + include firewall + } + + $allow = split("${::vagrant_gluster_allow}", ',') # ip list fact + + class { '::puppet::server': + pluginsync => true, # do we want to enable pluginsync? + storeconfigs => true, # do we want to enable storeconfigs? + autosign => [ + '*', # FIXME: this is a temporary solution + #"*.${domain}", # FIXME: this is a temporary solution + ], + #allow_duplicate_certs => true, # redeploy without cert clean + allow => $allow, # also used in fileserver.conf + repo => true, # automatic repos + shorewall => "${::vagrant_gluster_firewall}" ? { + 'false' => false, + default => true, + }, + start => true, + } + + class { '::puppet::deploy': + path => '/vagrant/puppet/', # puppet folder is put here... + backup => false, # don't use puppet to backup... + } +} + +node /^annex\d+$/ inherits default { # annex{1,2,..N} + + if "${::vagrant_gluster_firewall}" != 'false' { + include firewall + } + + class { '::puppet::client': + #start => true, + start => false, # useful for testing manually... + } + + # build a list of hashes with ordered vdX devices + # (s='';q=i;(q, r = (q - 1).divmod(26)) && s.prepend(('a'..'z').to_a[r]) until q.zero?;'/dev/vd'+s) + $skip = 1 # skip over 1 disk (eg: /dev/vda from the host) + $disks = "${::vagrant_gluster_disks}" + $disks_yaml = inline_template("<%= (1+@skip.to_i..@disks.to_i+@skip.to_i).collect { |i| { 'dev' => (s='';q=i;(q, r = (q - 1).divmod(26)) && s.insert(0, ('a'..'z').to_a[r]) until q.zero?;'/dev/vd'+s) } }.to_yaml %>") + #$brick_params_defaults = [ # this is one possible example data set + # {'dev' => '/dev/vdb'}, + # {'dev' => '/dev/vdc'}, + # {'dev' => '/dev/vdd'}, + # {'dev' => '/dev/vde'}, + #] + $brick_params_defaults = parseyaml($disks_yaml) + notice(inline_template('disks: <%= YAML::load(@disks_yaml).inspect %>')) + #notify { 'disks': + # message => inline_template('disks: <%= YAML::load(@disks_yaml).inspect %>'), + #} + + $brick_param_defaults = { + # TODO: set these from vagrant variables... + 'lvm' => false, + 'fstype' => "${::vagrant_gluster_fstype}" ? { + '' => undef, + default => "${::vagrant_gluster_fstype}", + }, + 'xfs_inode64' => true, + 'force' => true, + } + + # this is a simple way to setup gluster + class { '::gluster::simple': + volume => 'puppet', + replica => "${::vagrant_gluster_replica}", + count => "${::vagrant_gluster_bricks}", # brick count + layout => "${::vagrant_gluster_layout}", + vip => "${::vagrant_gluster_vip}", # from vagrant + version => "${::vagrant_gluster_version}", + vrrp => true, + setgroup => "${::vagrant_gluster_setgroup}", + shorewall => "${::vagrant_gluster_firewall}" ? { + 'false' => false, + default => true, + }, + # NOTE: this is brick_params_defaults NOT param! param is below + brick_params_defaults => "${::vagrant_gluster_disks}" ? { + '0' => undef, + # NOTE: _each_ host will have N bricks with these devs! + default => $brick_params_defaults, + }, + brick_param_defaults => "${::vagrant_gluster_disks}" ? { + '0' => undef, + # NOTE: _each_ brick will use these... + default => $brick_param_defaults, + }, + } +} + +node /^client\d+$/ inherits default { # client{1,2,..N} + + if "${::vagrant_gluster_firewall}" != 'false' { + include firewall + } + + class { '::puppet::client': + #start => true, + start => false, # useful for testing manually... + } + + $host = "${::vagrant_gluster_vip_fqdn}" ? { + '' => "${::vagrant_gluster_vip}", + default => "${::vagrant_gluster_vip_fqdn}", + } + + gluster::mount { '/mnt/gluster/puppet/': + server => "${host}:/puppet", + rw => true, + version => "${::vagrant_gluster_version}", + shorewall => "${::vagrant_gluster_firewall}" ? { + 'false' => false, + default => true, + }, + } +} + +class firewall { + + $FW = '$FW' # make using $FW in shorewall easier + + class { '::shorewall::configuration': + # NOTE: no configuration specifics are needed at the moment + } + + shorewall::zone { ['net', 'man']: + type => 'ipv4', + options => [], # these aren't really needed right now + } + + # management zone interface used by vagrant-libvirt + shorewall::interface { 'man': + interface => 'MAN_IF', + broadcast => 'detect', + physical => 'eth0', # XXX: set manually! + options => ['dhcp', 'tcpflags', 'routefilter', 'nosmurfs', 'logmartians'], + comment => 'Management zone.', # FIXME: verify options + } + + # XXX: eth1 'dummy' zone to trick vagrant-libvirt into leaving me alone + # + + # net zone that gluster uses to communicate + shorewall::interface { 'net': + interface => 'NET_IF', + broadcast => 'detect', + physical => 'eth2', # XXX: set manually! + options => ['tcpflags', 'routefilter', 'nosmurfs', 'logmartians'], + comment => 'Public internet zone.', # FIXME: verify options + } + + # TODO: is this policy really what we want ? can we try to limit this ? + shorewall::policy { '$FW-net': + policy => 'ACCEPT', # TODO: shouldn't we whitelist? + } + + shorewall::policy { '$FW-man': + policy => 'ACCEPT', # TODO: shouldn't we whitelist? + } + + #################################################################### + #ACTION SOURCE DEST PROTO DEST SOURCE ORIGINAL + # PORT PORT(S) DEST + shorewall::rule { 'ssh': rule => " + SSH/ACCEPT net $FW + SSH/ACCEPT man $FW + ", comment => 'Allow SSH'} + + shorewall::rule { 'ping': rule => " + #Ping/DROP net $FW + Ping/ACCEPT net $FW + Ping/ACCEPT man $FW + ", comment => 'Allow ping from the `bad` net zone'} + + shorewall::rule { 'icmp': rule => " + ACCEPT $FW net icmp + ACCEPT $FW man icmp + ", comment => 'Allow icmp from the firewall zone'} +} + diff --git a/gluster/vagrant/puppet/modules/.gitignore b/gluster/vagrant/puppet/modules/.gitignore new file mode 100644 index 000000000..8a495bf49 --- /dev/null +++ b/gluster/vagrant/puppet/modules/.gitignore @@ -0,0 +1 @@ +gluster/ diff --git a/gluster/vagrant/puppet/modules/Makefile b/gluster/vagrant/puppet/modules/Makefile new file mode 100644 index 000000000..c6507733a --- /dev/null +++ b/gluster/vagrant/puppet/modules/Makefile @@ -0,0 +1,67 @@ +# Makefile for pulling in git modules for Vagrant deployment for Puppet-Gluster +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: if we remove a module, it won't get purged from the destination! + +# NOTE: this script can sync puppet-gluster to a specific sha1sum commit, or it +# can sync all of the repos to git master. This option can be useful for devel. + +BASE = 'https://github.com/purpleidea' +MODULES := \ + puppet-common \ + puppet-gluster \ + puppet-keepalived \ + puppet-module-data \ + puppet-puppet \ + puppet-shorewall \ + puppet-yum \ + puppetlabs-apt \ + puppetlabs-stdlib +# NOTE: set to a git commit id if we need an specific commit for vagrant builds +# NOTE: remember that new commits to master should change this to a specific id +# if they will break the vagrant build process. hopefully we don't forget this! +#SHA1SUM := master +SHA1SUM := $(shell git rev-parse --verify HEAD) # goto whatever the main tree is at + +.PHONY: all modules gluster +.SILENT: all modules gluster + +all: + +# +# modules +# +# clone, and then pull +modules: + basename `pwd` | grep -q '^modules' || exit 1 # run in a modules dir! + for i in $(MODULES); do \ + j=`echo $$i | awk -F '-' '{print $$2}'`; \ + [ -d "$$j" ] || git clone --depth 1 $(BASE)/$$i.git $$j; \ + [ -d "$$j" ] && cd $$j && git pull; cd ..; \ + done + +# +# gluster +# +# just clone and pull this one +gluster: + basename `pwd` | grep -q '^modules' || exit 1 # run in a modules dir! + i='puppet-gluster'; \ + j=`echo $$i | awk -F '-' '{print $$2}'`; \ + [ -d "$$j" ] || git clone ../../../. $$j; \ + [ -d "$$j" ] && cd $$j && git checkout master && git pull && git checkout $(SHA1SUM); cd .. + diff --git a/gluster/vagrant/puppet/modules/README b/gluster/vagrant/puppet/modules/README new file mode 100644 index 000000000..c1837a538 --- /dev/null +++ b/gluster/vagrant/puppet/modules/README @@ -0,0 +1,22 @@ +This directory contains the puppet (git) module dependencies for Puppet-Gluster. They are included as git submodules for convenience and version compatibility. The Puppet-Gluster module itself is cloned in by a Makefile on provisioning. + +The one problem is Puppet-Gluster itself, since it is the parent repository to this sub-directory. There were a few options: + +1) Maintain the vagrant/ directory as a separate git project. +This would make a lot of sense, but I wanted to keep the vagrant portions bundled with Puppet-Gluster since they are so closely connected and for ease of distribution. In this situation, the vagrant/puppet/modules/ directory would include the Puppet-Gluster submodule along with all the other puppet (git) modules. + +2) Fill the vagrant/puppet/modules/ directory with git submodules. +This would make a lot of sense because you can reference specific commits, and it's easy to recursively clone all of the necessary code for a vagrant run. The problem is that recursively referencing the Puppet-Gluster might be a little awkward for some hackers to understand. One inconvenience, is that to update the modules/ directory, you'd have to first push your code changes to the server, get the sha1 commit hash, and then in a secondary commit change the submodules pointer. This would apparently cause a cascase of extra cloning each new commit. + +3) Fill the vagrant/puppet/modules/ directory with git submodules & 1 symlink. +This option seems to be the best solution. As in #2, we use git submodules. For the tricky Puppet-Gluster recursion scenario, we symlink the correct parent directory so that the relevant puppet code is present for the puppet::deploy. This only works if the provisioner follows the symlinks. For vagrant-libvirt, rsync needs the --copy-dirlinks option added. + +4) Maintain a Makefile and sync in Puppet-Gluster as needed. +This is what I've adopted for now. It works, and is mostly straightforward. If you can find a better solution, please let me know! + +Hope this gives you some helpful background, and thanks to #git for consulting. + +Happy hacking, + +James + diff --git a/gluster/vagrant/puppet/modules/apt b/gluster/vagrant/puppet/modules/apt new file mode 160000 index 000000000..5b54eda36 --- /dev/null +++ b/gluster/vagrant/puppet/modules/apt @@ -0,0 +1 @@ +Subproject commit 5b54eda3668a42b12e21696e500b40a27005bf2b diff --git a/common b/gluster/vagrant/puppet/modules/common similarity index 100% rename from common rename to gluster/vagrant/puppet/modules/common diff --git a/gluster/vagrant/puppet/modules/keepalived b/gluster/vagrant/puppet/modules/keepalived new file mode 160000 index 000000000..f0585c45e --- /dev/null +++ b/gluster/vagrant/puppet/modules/keepalived @@ -0,0 +1 @@ +Subproject commit f0585c45e7c5d5c2d86d0a84690a75dc522b2cd4 diff --git a/gluster/vagrant/puppet/modules/module-data b/gluster/vagrant/puppet/modules/module-data new file mode 160000 index 000000000..4b3ad1cc2 --- /dev/null +++ b/gluster/vagrant/puppet/modules/module-data @@ -0,0 +1 @@ +Subproject commit 4b3ad1cc239d7831616e69796184e400de0f5fe4 diff --git a/gluster/vagrant/puppet/modules/puppet b/gluster/vagrant/puppet/modules/puppet new file mode 160000 index 000000000..f139d0b7c --- /dev/null +++ b/gluster/vagrant/puppet/modules/puppet @@ -0,0 +1 @@ +Subproject commit f139d0b7cfe6d55c0848d0d338e19fe640a961f2 diff --git a/gluster/vagrant/puppet/modules/shorewall b/gluster/vagrant/puppet/modules/shorewall new file mode 160000 index 000000000..fbc7c6576 --- /dev/null +++ b/gluster/vagrant/puppet/modules/shorewall @@ -0,0 +1 @@ +Subproject commit fbc7c6576092ceaf81b837989e086cb7fcc071d8 diff --git a/gluster/vagrant/puppet/modules/stdlib b/gluster/vagrant/puppet/modules/stdlib new file mode 160000 index 000000000..44c181ec0 --- /dev/null +++ b/gluster/vagrant/puppet/modules/stdlib @@ -0,0 +1 @@ +Subproject commit 44c181ec0e230768b8dce10de57f9b32638e66e1 diff --git a/gluster/vagrant/puppet/modules/yum b/gluster/vagrant/puppet/modules/yum new file mode 160000 index 000000000..d098f6de9 --- /dev/null +++ b/gluster/vagrant/puppet/modules/yum @@ -0,0 +1 @@ +Subproject commit d098f6de9a38055b3482325d371722835b576976 diff --git a/haproxy b/haproxy deleted file mode 160000 index f381510e9..000000000 --- a/haproxy +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f381510e940ee11feb044c1c728ba2e5af807c79 diff --git a/haproxy/.fixtures.yml b/haproxy/.fixtures.yml new file mode 100644 index 000000000..8d6f22d6f --- /dev/null +++ b/haproxy/.fixtures.yml @@ -0,0 +1,5 @@ +fixtures: + repositories: + concat: "git://github.com/ripienaar/puppet-concat.git" + symlinks: + haproxy: "#{source_dir}" diff --git a/haproxy/.gemfile b/haproxy/.gemfile new file mode 100644 index 000000000..9aad840c0 --- /dev/null +++ b/haproxy/.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/haproxy/.travis.yml b/haproxy/.travis.yml new file mode 100644 index 000000000..fdbc95dc6 --- /dev/null +++ b/haproxy/.travis.yml @@ -0,0 +1,23 @@ +language: ruby +rvm: + - 1.8.7 + - 1.9.3 +script: "rake spec" +branches: + only: + - master +env: + - PUPPET_VERSION=2.6.17 + - PUPPET_VERSION=2.7.19 + #- PUPPET_VERSION=3.0.1 # Breaks due to rodjek/rspec-puppet#58 +notifications: + email: false +gemfile: .gemfile +matrix: + exclude: + - rvm: 1.9.3 + gemfile: .gemfile + env: PUPPET_VERSION=2.6.17 + - rvm: 1.8.7 + gemfile: .gemfile + env: PUPPET_VERSION=3.0.1 diff --git a/haproxy/CHANGELOG b/haproxy/CHANGELOG new file mode 100644 index 000000000..99a992b2f --- /dev/null +++ b/haproxy/CHANGELOG @@ -0,0 +1,18 @@ +2013-05-25 - Version 0.3.0 +Features: +- Add travis testing +- Add `haproxy::basancermember` `define_cookies` parameter +- Add array support to `haproxy::listen` `ipaddress` parameter + +Bugfixes: +- Documentation +- Listen -> Balancermember dependency +- Config line ordering +- Whitespace +- Add template lines for `haproxy::listen` `mode` parameter + +2012-10-12 - Version 0.2.0 +- Initial public release +- Backwards incompatible changes all around +- No longer needs ordering passed for more than one listener +- Accepts multiple listen ips/ports/server_names diff --git a/haproxy/Modulefile b/haproxy/Modulefile new file mode 100644 index 000000000..7f70ac0c2 --- /dev/null +++ b/haproxy/Modulefile @@ -0,0 +1,12 @@ +name 'puppetlabs-haproxy' +version '0.3.0' +source 'git://github.com/puppetlabs/puppetlabs-haproxy' +author 'Puppet Labs' +license 'Apache License, Version 2.0' +summary 'Haproxy Module' +description 'An Haproxy module for Redhat family OSes using Storeconfigs' +project_page 'http://github.com/puppetlabs/puppetlabs-haproxy' + +## Add dependencies, if any: +# dependency 'username/name', '>= 1.2.0' +dependency 'ripienaar/concat', '>= 0.1.0' diff --git a/haproxy/README.md b/haproxy/README.md new file mode 100644 index 000000000..8e51edc5c --- /dev/null +++ b/haproxy/README.md @@ -0,0 +1,93 @@ +PuppetLabs Module for haproxy +============================= + +HAProxy is an HA proxying daemon for load-balancing to clustered services. It +can proxy TCP directly, or other kinds of traffic such as HTTP. + +Basic Usage +----------- + +This haproxy uses storeconfigs to collect and realize balancer member servers +on a load balancer server. Currently Redhat family OSes are supported. + +*To install and configure HAProxy server listening on port 80* + +```puppet +node 'haproxy-server' { + class { 'haproxy': } + haproxy::listen { 'puppet00': + ipaddress => $::ipaddress, + ports => '8140', + } +} +``` + +*To add backend loadbalance members* + +```puppet +node 'webserver01' { + @@haproxy::balancermember { $fqdn: + listening_service => 'puppet00', + server_names => $::hostname, + ipaddresses => $::ipaddress, + ports => '8140', + options => 'check' + } +} +``` + +Configuring haproxy options +--------------------------- + +The base `haproxy` class can accept two parameters which will configure basic +behaviour of the haproxy server daemon: + +- `global_options` to configure the `global` section in `haproxy.cfg` +- `defaults_options` to configure the `defaults` section in `haproxy.cfg` + +Configuring haproxy daemon listener +----------------------------------- + +One `haproxy::listen` defined resource should be defined for each HAProxy loadbalanced set of backend servers. The title of the `haproxy::listen` resource is the key to which balancer members will be proxied to. The `ipaddress` field should be the public ip address which the loadbalancer will be contacted on. The `ports` attribute can accept an array or comma-separated list of ports which should be proxied to the `haproxy::balancermember` nodes. + +Configuring haproxy daemon frontend +----------------------------------- + +One `haproxy::frontend` defined resource should be defined for each HAProxy front end you wish to set up. The `ipaddress` field should be the public ip address which the loadbalancer will be contacted on. The `ports` attribute can accept an array or comma-separated list of ports which should be proxied to the `haproxy::backend` resources. + +Configuring haproxy daemon backend +---------------------------------- + +One `haproxy::backend` defined resource should be defined for each HAProxy loadbalanced set of backend servers. The title of the `haproxy::backend` resource is the key to which balancer members will be proxied to. Note that an `haproxy::listen` resource and `haproxy::backend` resource *can* have the same name, and any balancermembers exported to that name will show up in both places. This is likely to have unsatisfactory results, but there's nothing preventing this from happening. + +Configuring haproxy loadbalanced member nodes +--------------------------------------------- + +The `haproxy::balancermember` defined resource should be exported from each node +which is serving loadbalanced traffic. the `listening_service` attribute will +associate it with `haproxy::listen` directives on the haproxy node. +`ipaddresses` and `ports` will be assigned to the member to be contacted on. If an array of `ipaddresses` and `server_names` are provided then they will be added to the config in lock-step. + +Dependencies +------------ + +Tested and built on Ubuntu and CentOS + +Copyright and License +--------------------- + +Copyright (C) 2012 [Puppet Labs](https://www.puppetlabs.com/) Inc + +Puppet Labs can be contacted at: info@puppetlabs.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/haproxy/Rakefile b/haproxy/Rakefile new file mode 100644 index 000000000..cd3d37995 --- /dev/null +++ b/haproxy/Rakefile @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/rake_tasks' diff --git a/haproxy/manifests/backend.pp b/haproxy/manifests/backend.pp new file mode 100644 index 000000000..3ae6781bd --- /dev/null +++ b/haproxy/manifests/backend.pp @@ -0,0 +1,76 @@ +# == Define Resource Type: haproxy::backend +# +# This type will setup a backend service configuration block inside the +# haproxy.cfg file on an haproxy load balancer. Each backend service needs one +# or more backend member servers (that can be declared with the +# haproxy::balancermember defined resource type). Using storeconfigs, you can +# export the haproxy::balancermember resources on all load balancer member +# servers and then collect them on a single haproxy load balancer server. +# +# === Requirement/Dependencies: +# +# Currently requires the ripienaar/concat module on the Puppet Forge and +# uses storeconfigs on the Puppet Master to export/collect resources +# from all backend members. +# +# === Parameters +# +# [*name*] +# The namevar of the defined resource type is the backend service's name. +# This name goes right after the 'backend' statement in haproxy.cfg +# +# [*options*] +# A hash of options that are inserted into the backend configuration block. +# +# [*collect_exported*] +# Boolean, default 'true'. True means 'collect exported @@balancermember +# resources' (for the case when every balancermember node exports itself), +# false means 'rely on the existing declared balancermember resources' (for +# the case when you know the full set of balancermember in advance and use +# haproxy::balancermember with array arguments, which allows you to deploy +# everything in 1 run) +# +# +# === Examples +# +# Exporting the resource for a backend member: +# +# haproxy::backend { 'puppet00': +# options => { +# 'option' => [ +# 'tcplog', +# 'ssl-hello-chk' +# ], +# 'balance' => 'roundrobin' +# }, +# } +# +# === Authors +# +# Gary Larizza +# Jeremy Kitchen +# +define haproxy::backend ( + $collect_exported = true, + $options = { + 'option' => [ + 'tcplog', + 'ssl-hello-chk' + ], + 'balance' => 'roundrobin' + } +) { + + # Template uses: $name, $ipaddress, $ports, $options + concat::fragment { "${name}_backend_block": + order => "20-${name}-00", + target => '/etc/haproxy/haproxy.cfg', + content => template('haproxy/haproxy_backend_block.erb'), + } + + if $collect_exported { + Haproxy::Balancermember <<| listening_service == $name |>> + } + # else: the resources have been created and they introduced their + # concat fragments. We don't have to do anything about them. +} diff --git a/haproxy/manifests/balancermember.pp b/haproxy/manifests/balancermember.pp new file mode 100644 index 000000000..4fe949ae8 --- /dev/null +++ b/haproxy/manifests/balancermember.pp @@ -0,0 +1,105 @@ +# == Define Resource Type: haproxy::balancermember +# +# This type will setup a balancer member inside a listening service +# configuration block in /etc/haproxy/haproxy.cfg on the load balancer. +# currently it only has the ability to specify the instance name, +# ip address, port, and whether or not it is a backup. More features +# can be added as needed. The best way to implement this is to export +# this resource for all haproxy balancer member servers, and then collect +# them on the main haproxy load balancer. +# +# === Requirement/Dependencies: +# +# Currently requires the ripienaar/concat module on the Puppet Forge and +# uses storeconfigs on the Puppet Master to export/collect resources +# from all balancer members. +# +# === Parameters +# +# [*name*] +# The title of the resource is arbitrary and only utilized in the concat +# fragment name. +# +# [*listening_service*] +# The haproxy service's instance name (or, the title of the +# haproxy::listen resource). This must match up with a declared +# haproxy::listen resource. +# +# [*ports*] +# An array or commas-separated list of ports for which the balancer member +# will accept connections from the load balancer. Note that cookie values +# aren't yet supported, but shouldn't be difficult to add to the +# configuration. If you use an array in server_names and ipaddresses, the +# same port is used for all balancermembers. +# +# [*server_names*] +# The name of the balancer member server as known to haproxy in the +# listening service's configuration block. This defaults to the +# hostname. Can be an array of the same length as ipaddresses, +# in which case a balancermember is created for each pair of +# server_names and ipaddresses (in lockstep). +# +# [*ipaddresses*] +# The ip address used to contact the balancer member server. +# Can be an array, see documentation to server_names. +# +# [*ensure*] +# If the balancermember should be present or absent. +# Defaults to present. +# +# [*options*] +# An array of options to be specified after the server declaration +# in the listening service's configuration block. +# +# [*define_cookies*] +# If true, then add "cookie SERVERID" stickiness options. +# Default false. +# +# === Examples +# +# Exporting the resource for a balancer member: +# +# @@haproxy::balancermember { 'haproxy': +# listening_service => 'puppet00', +# ports => '8140', +# server_names => $::hostname, +# ipaddresses => $::ipaddress, +# options => 'check', +# } +# +# +# Collecting the resource on a load balancer +# +# Haproxy::Balancermember <<| listening_service == 'puppet00' |>> +# +# Creating the resource for multiple balancer members at once +# (for single-pass installation of haproxy without requiring a first +# pass to export the resources if you know the members in advance): +# +# haproxy::balancermember { 'haproxy': +# listening_service => 'puppet00', +# ports => '8140', +# server_names => ['server01', 'server02'], +# ipaddresses => ['192.168.56.200', '192.168.56.201'], +# options => 'check', +# } +# +# (this resource can be declared anywhere) +# +define haproxy::balancermember ( + $listening_service, + $ports, + $server_names = $::hostname, + $ipaddresses = $::ipaddress, + $ensure = 'present', + $options = '', + $define_cookies = false +) { + # Template uses $ipaddresses, $server_name, $ports, $option + concat::fragment { "${listening_service}_balancermember_${name}": + order => "20-${listening_service}-${name}", + ensure => $ensure, + target => '/etc/haproxy/haproxy.cfg', + content => template('haproxy/haproxy_balancermember.erb'), + } +} diff --git a/haproxy/manifests/frontend.pp b/haproxy/manifests/frontend.pp new file mode 100644 index 000000000..488acaf0f --- /dev/null +++ b/haproxy/manifests/frontend.pp @@ -0,0 +1,76 @@ +# == Define Resource Type: haproxy::frontend +# +# This type will setup a frontend service configuration block inside +# the haproxy.cfg file on an haproxy load balancer. +# +# === Requirement/Dependencies: +# +# Currently requires the ripienaar/concat module on the Puppet Forge and +# uses storeconfigs on the Puppet Master to export/collect resources +# from all balancer members. +# +# === Parameters +# +# [*name*] +# The namevar of the defined resource type is the frontend service's name. +# This name goes right after the 'frontend' statement in haproxy.cfg +# +# [*ports*] +# Ports on which the proxy will listen for connections on the ip address +# specified in the ipaddress parameter. Accepts either a single +# comma-separated string or an array of strings which may be ports or +# hyphenated port ranges. +# +# [*ipaddress*] +# The ip address the proxy binds to. Empty addresses, '*', and '0.0.0.0' +# mean that the proxy listens to all valid addresses on the system. +# +# [*mode*] +# The mode of operation for the frontend service. Valid values are undef, +# 'tcp', 'http', and 'health'. +# +# [*options*] +# A hash of options that are inserted into the frontend service +# configuration block. +# +# === Examples +# +# Exporting the resource for a balancer member: +# +# haproxy::frontend { 'puppet00': +# ipaddress => $::ipaddress, +# ports => '18140', +# mode => 'tcp', +# options => { +# 'option' => [ +# 'tcplog', +# 'accept-invalid-http-request', +# ], +# 'timeout client' => '30', +# 'balance' => 'roundrobin' +# }, +# } +# +# === Authors +# +# Gary Larizza +# +define haproxy::frontend ( + $ports, + $ipaddress = [$::ipaddress], + $mode = undef, + $collect_exported = true, + $options = { + 'option' => [ + 'tcplog', + ], + } +) { + # Template uses: $name, $ipaddress, $ports, $options + concat::fragment { "${name}_frontend_block": + order => "15-${name}-00", + target => '/etc/haproxy/haproxy.cfg', + content => template('haproxy/haproxy_frontend_block.erb'), + } + +} diff --git a/haproxy/manifests/init.pp b/haproxy/manifests/init.pp new file mode 100644 index 000000000..8fb60d7e9 --- /dev/null +++ b/haproxy/manifests/init.pp @@ -0,0 +1,150 @@ +# == Class: haproxy +# +# A Puppet module, using storeconfigs, to model an haproxy configuration. +# Currently VERY limited - assumes Redhat/CentOS setup. Pull requests accepted! +# +# === Requirement/Dependencies: +# +# Currently requires the ripienaar/concat module on the Puppet Forge and +# uses storeconfigs on the Puppet Master to export/collect resources +# from all balancer members. +# +# === Parameters +# +# [*enable*] +# Chooses whether haproxy should be installed or ensured absent. +# Currently ONLY accepts valid boolean true/false values. +# +# [*global_options*] +# A hash of all the haproxy global options. If you want to specify more +# than one option (i.e. multiple timeout or stats options), pass those +# options as an array and you will get a line for each of them in the +# resultant haproxy.cfg file. +# +# [*defaults_options*] +# A hash of all the haproxy defaults options. If you want to specify more +# than one option (i.e. multiple timeout or stats options), pass those +# options as an array and you will get a line for each of them in the +# resultant haproxy.cfg file. +# +# +# === Examples +# +# class { 'haproxy': +# enable => true, +# global_options => { +# 'log' => "${::ipaddress} local0", +# 'chroot' => '/var/lib/haproxy', +# 'pidfile' => '/var/run/haproxy.pid', +# 'maxconn' => '4000', +# 'user' => 'haproxy', +# 'group' => 'haproxy', +# 'daemon' => '', +# 'stats' => 'socket /var/lib/haproxy/stats' +# }, +# defaults_options => { +# 'log' => 'global', +# 'stats' => 'enable', +# 'option' => 'redispatch', +# 'retries' => '3', +# 'timeout' => [ +# 'http-request 10s', +# 'queue 1m', +# 'connect 10s', +# 'client 1m', +# 'server 1m', +# 'check 10s' +# ], +# 'maxconn' => '8000' +# }, +# } +# +class haproxy ( + $manage_service = true, + $enable = true, + $global_options = $haproxy::params::global_options, + $defaults_options = $haproxy::params::defaults_options +) inherits haproxy::params { + include concat::setup + + package { 'haproxy': + ensure => $enable ? { + true => present, + false => absent, + }, + name => 'haproxy', + } + + if $enable { + concat { '/etc/haproxy/haproxy.cfg': + owner => '0', + group => '0', + mode => '0644', + require => Package['haproxy'], + notify => $manage_service ? { + true => Service['haproxy'], + false => undef, + }, + } + + # Simple Header + concat::fragment { '00-header': + target => '/etc/haproxy/haproxy.cfg', + order => '01', + content => "# This file managed by Puppet\n", + } + + # Template uses $global_options, $defaults_options + concat::fragment { 'haproxy-base': + target => '/etc/haproxy/haproxy.cfg', + order => '10', + content => template('haproxy/haproxy-base.cfg.erb'), + } + + if ($::osfamily == 'Debian') { + file { '/etc/default/haproxy': + content => 'ENABLED=1', + require => Package['haproxy'], + before => $manage_service ? { + true => Service['haproxy'], + false => undef, + }, + } + } + + if $global_options['chroot'] { + file { $global_options['chroot']: + ensure => directory, + } + } + + } + + if $manage_service { + if $global_options['chroot'] { + $deps = [ + Concat['/etc/haproxy/haproxy.cfg'], + File[$global_options['chroot']], + ] + } else { + $deps = [ + Concat['/etc/haproxy/haproxy.cfg'], + ] + } + + service { 'haproxy': + ensure => $enable ? { + true => running, + false => stopped, + }, + enable => $enable ? { + true => true, + false => false, + }, + name => 'haproxy', + hasrestart => true, + hasstatus => true, + require => $deps, + } + } +} diff --git a/haproxy/manifests/listen.pp b/haproxy/manifests/listen.pp new file mode 100644 index 000000000..5e2fc762f --- /dev/null +++ b/haproxy/manifests/listen.pp @@ -0,0 +1,95 @@ +# == Define Resource Type: haproxy::listen +# +# This type will setup a listening service configuration block inside +# the haproxy.cfg file on an haproxy load balancer. Each listening service +# configuration needs one or more load balancer member server (that can be +# declared with the haproxy::balancermember defined resource type). Using +# storeconfigs, you can export the haproxy::balancermember resources on all +# load balancer member servers, and then collect them on a single haproxy +# load balancer server. +# +# === Requirement/Dependencies: +# +# Currently requires the ripienaar/concat module on the Puppet Forge and +# uses storeconfigs on the Puppet Master to export/collect resources +# from all balancer members. +# +# === Parameters +# +# [*name*] +# The namevar of the defined resource type is the listening service's name. +# This name goes right after the 'listen' statement in haproxy.cfg +# +# [*ports*] +# Ports on which the proxy will listen for connections on the ip address +# specified in the ipaddress parameter. Accepts either a single +# comma-separated string or an array of strings which may be ports or +# hyphenated port ranges. +# +# [*ipaddress*] +# The ip address the proxy binds to. Empty addresses, '*', and '0.0.0.0' +# mean that the proxy listens to all valid addresses on the system. +# +# [*mode*] +# The mode of operation for the listening service. Valid values are undef, +# 'tcp', 'http', and 'health'. +# +# [*options*] +# A hash of options that are inserted into the listening service +# configuration block. +# +# [*collect_exported*] +# Boolean, default 'true'. True means 'collect exported @@balancermember resources' +# (for the case when every balancermember node exports itself), false means +# 'rely on the existing declared balancermember resources' (for the case when you +# know the full set of balancermembers in advance and use haproxy::balancermember +# with array arguments, which allows you to deploy everything in 1 run) +# +# +# === Examples +# +# Exporting the resource for a balancer member: +# +# haproxy::listen { 'puppet00': +# ipaddress => $::ipaddress, +# ports => '18140', +# mode => 'tcp', +# options => { +# 'option' => [ +# 'tcplog', +# 'ssl-hello-chk' +# ], +# 'balance' => 'roundrobin' +# }, +# } +# +# === Authors +# +# Gary Larizza +# +define haproxy::listen ( + $ports, + $ipaddress = [$::ipaddress], + $mode = undef, + $collect_exported = true, + $options = { + 'option' => [ + 'tcplog', + 'ssl-hello-chk' + ], + 'balance' => 'roundrobin' + } +) { + # Template uses: $name, $ipaddress, $ports, $options + concat::fragment { "${name}_listen_block": + order => "20-${name}-00", + target => '/etc/haproxy/haproxy.cfg', + content => template('haproxy/haproxy_listen_block.erb'), + } + + if $collect_exported { + Haproxy::Balancermember <<| listening_service == $name |>> + } + # else: the resources have been created and they introduced their + # concat fragments. We don't have to do anything about them. +} diff --git a/haproxy/manifests/params.pp b/haproxy/manifests/params.pp new file mode 100644 index 000000000..53442ddcc --- /dev/null +++ b/haproxy/manifests/params.pp @@ -0,0 +1,65 @@ +# == Class: haproxy::params +# +# This is a container class holding default parameters for for haproxy class. +# currently, only the Redhat family is supported, but this can be easily +# extended by changing package names and configuration file paths. +# +class haproxy::params { + case $osfamily { + Redhat: { + $global_options = { + 'log' => "${::ipaddress} local0", + 'chroot' => '/var/lib/haproxy', + 'pidfile' => '/var/run/haproxy.pid', + 'maxconn' => '4000', + 'user' => 'haproxy', + 'group' => 'haproxy', + 'daemon' => '', + 'stats' => 'socket /var/lib/haproxy/stats' + } + $defaults_options = { + 'log' => 'global', + 'stats' => 'enable', + 'option' => 'redispatch', + 'retries' => '3', + 'timeout' => [ + 'http-request 10s', + 'queue 1m', + 'connect 10s', + 'client 1m', + 'server 1m', + 'check 10s', + ], + 'maxconn' => '8000' + } + } + Debian: { + $global_options = { + 'log' => "${::ipaddress} local0", + 'chroot' => '/var/lib/haproxy', + 'pidfile' => '/var/run/haproxy.pid', + 'maxconn' => '4000', + 'user' => 'haproxy', + 'group' => 'haproxy', + 'daemon' => '', + 'stats' => 'socket /var/lib/haproxy/stats' + } + $defaults_options = { + 'log' => 'global', + 'stats' => 'enable', + 'option' => 'redispatch', + 'retries' => '3', + 'timeout' => [ + 'http-request 10s', + 'queue 1m', + 'connect 10s', + 'client 1m', + 'server 1m', + 'check 10s', + ], + 'maxconn' => '8000' + } + } + default: { fail("The $::osfamily operating system is not supported with the haproxy module") } + } +} diff --git a/haproxy/spec/classes/haproxy_spec.rb b/haproxy/spec/classes/haproxy_spec.rb new file mode 100644 index 000000000..4b5902ce5 --- /dev/null +++ b/haproxy/spec/classes/haproxy_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe 'haproxy', :type => :class do + let(:default_facts) do + { + :concat_basedir => '/dne', + :ipaddress => '10.10.10.10' + } + end + context 'on supported platforms' do + describe 'for OS-agnostic configuration' do + ['Debian', 'RedHat'].each do |osfamily| + context "on #{osfamily} family operatingsystems" do + let(:facts) do + { :osfamily => osfamily }.merge default_facts + end + let(:params) do + {'enable' => true} + end + it { should include_class('concat::setup') } + it 'should install the haproxy package' do + subject.should contain_package('haproxy').with( + 'ensure' => 'present' + ) + end + it 'should install the haproxy service' do + subject.should contain_service('haproxy').with( + 'ensure' => 'running', + 'enable' => 'true', + 'hasrestart' => 'true', + 'hasstatus' => 'true', + 'require' => [ + 'Concat[/etc/haproxy/haproxy.cfg]', + 'File[/var/lib/haproxy]' + ] + ) + end + it 'should set up /etc/haproxy/haproxy.cfg as a concat resource' do + subject.should contain_concat('/etc/haproxy/haproxy.cfg').with( + 'owner' => '0', + 'group' => '0', + 'mode' => '0644' + ) + end + it 'should manage the chroot directory' do + subject.should contain_file('/var/lib/haproxy').with( + 'ensure' => 'directory' + ) + end + it 'should contain a header concat fragment' do + subject.should contain_concat__fragment('00-header').with( + 'target' => '/etc/haproxy/haproxy.cfg', + 'order' => '01', + 'content' => "# This file managed by Puppet\n" + ) + end + it 'should contain a haproxy-base concat fragment' do + subject.should contain_concat__fragment('haproxy-base').with( + 'target' => '/etc/haproxy/haproxy.cfg', + 'order' => '10' + ) + end + describe 'Base concat fragment contents' do + let(:contents) { param_value(subject, 'concat::fragment', 'haproxy-base', 'content').split("\n") } + it 'should contain global and defaults sections' do + contents.should include('global') + contents.should include('defaults') + end + it 'should log to an ip address for local0' do + contents.should be_any { |match| match =~ / log \d+(\.\d+){3} local0/ } + end + it 'should specify the default chroot' do + contents.should include(' chroot /var/lib/haproxy') + end + it 'should specify the correct user' do + contents.should include(' user haproxy') + end + it 'should specify the correct group' do + contents.should include(' group haproxy') + end + it 'should specify the correct pidfile' do + contents.should include(' pidfile /var/run/haproxy.pid') + end + end + end + context "on #{osfamily} family operatingsystems without managing the service" do + let(:facts) do + { :osfamily => osfamily }.merge default_facts + end + let(:params) do + { + 'enable' => true, + 'manage_service' => false, + } + end + it { should include_class('concat::setup') } + it 'should install the haproxy package' do + subject.should contain_package('haproxy').with( + 'ensure' => 'present' + ) + end + it 'should install the haproxy service' do + subject.should_not contain_service('haproxy') + end + end + end + end + describe 'for OS-specific configuration' do + context 'only on Debian family operatingsystems' do + let(:facts) do + { :osfamily => 'Debian' }.merge default_facts + end + it 'should manage haproxy service defaults' do + subject.should contain_file('/etc/default/haproxy').with( + 'before' => 'Service[haproxy]', + 'require' => 'Package[haproxy]' + ) + verify_contents(subject, '/etc/default/haproxy', ['ENABLED=1']) + end + end + context 'only on RedHat family operatingsystems' do + let(:facts) do + { :osfamily => 'RedHat' }.merge default_facts + end + end + end + end + context 'on unsupported operatingsystems' do + let(:facts) do + { :osfamily => 'RainbowUnicorn' }.merge default_facts + end + it do + expect { + should contain_service('haproxy') + }.to raise_error(Puppet::Error, /operating system is not supported with the haproxy module/) + end + end +end diff --git a/haproxy/spec/defines/backend_spec.rb b/haproxy/spec/defines/backend_spec.rb new file mode 100644 index 000000000..b672d7689 --- /dev/null +++ b/haproxy/spec/defines/backend_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe 'haproxy::backend' do + let(:title) { 'tyler' } + let(:facts) {{ :ipaddress => '1.1.1.1' }} + + context "when no options are passed" do + let (:params) do + { + :name => 'bar' + } + end + + it { should contain_concat__fragment('bar_backend_block').with( + 'order' => '20-bar-00', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => "\nbackend bar\n balance roundrobin\n option tcplog\n option ssl-hello-chk\n" + ) } + end + + + +end + + diff --git a/haproxy/spec/defines/balancermember_spec.rb b/haproxy/spec/defines/balancermember_spec.rb new file mode 100644 index 000000000..4da2815e2 --- /dev/null +++ b/haproxy/spec/defines/balancermember_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe 'haproxy::balancermember' do + let(:title) { 'tyler' } + let(:facts) do + { + :ipaddress => '1.1.1.1', + :hostname => 'dero' + } + end + + context 'with a single balancermember option' do + let(:params) do + { + :name => 'tyler', + :listening_service => 'croy', + :ports => '18140', + :options => 'check' + } + end + + it { should contain_concat__fragment('croy_balancermember_tyler').with( + 'order' => '20-croy-tyler', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => " server dero 1.1.1.1:18140 check\n" + ) } + end + + context 'with a balancermember with ensure => absent ' do + let(:params) do + { + :name => 'tyler', + :listening_service => 'croy', + :ports => '18140', + :ensure => 'absent' + } + end + + it { should contain_concat__fragment('croy_balancermember_tyler').with( + 'order' => '20-croy-tyler', + 'target' => '/etc/haproxy/haproxy.cfg', + 'ensure' => 'absent', + 'content' => " server dero 1.1.1.1:18140 \n" + ) } + end + + context 'with multiple balancermember options' do + let(:params) do + { + :name => 'tyler', + :listening_service => 'croy', + :ports => '18140', + :options => ['check', 'close'] + } + end + + it { should contain_concat__fragment('croy_balancermember_tyler').with( + 'order' => '20-croy-tyler', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => " server dero 1.1.1.1:18140 check close\n" + ) } + end + + context 'with cookie and multiple balancermember options' do + let(:params) do + { + :name => 'tyler', + :listening_service => 'croy', + :ports => '18140', + :options => ['check', 'close'], + :define_cookies => true + } + end + + it { should contain_concat__fragment('croy_balancermember_tyler').with( + 'order' => '20-croy-tyler', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => " server dero 1.1.1.1:18140 cookie dero check close\n" + ) } + end + context 'with multiple servers' do + let(:params) do + { + :name => 'tyler', + :listening_service => 'croy', + :ports => '18140', + :server_names => ['server01', 'server02'], + :ipaddresses => ['192.168.56.200', '192.168.56.201'], + :options => ['check'] + } + end + + it { should contain_concat__fragment('croy_balancermember_tyler').with( + 'order' => '20-croy-tyler', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => " server server01 192.168.56.200:18140 check\n server server02 192.168.56.201:18140 check\n" + ) } + end + context 'with multiple servers and multiple ports' do + let(:params) do + { + :name => 'tyler', + :listening_service => 'croy', + :ports => ['18140','18150'], + :server_names => ['server01', 'server02'], + :ipaddresses => ['192.168.56.200', '192.168.56.201'], + :options => ['check'] + } + end + + it { should contain_concat__fragment('croy_balancermember_tyler').with( + 'order' => '20-croy-tyler', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => " server server01 192.168.56.200:18140,192.168.56.200:18150 check\n server server02 192.168.56.201:18140,192.168.56.201:18150 check\n" + ) } + end +end diff --git a/haproxy/spec/defines/frontend_spec.rb b/haproxy/spec/defines/frontend_spec.rb new file mode 100644 index 000000000..04acce86e --- /dev/null +++ b/haproxy/spec/defines/frontend_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'haproxy::frontend' do + let(:title) { 'tyler' } + let(:facts) {{ :ipaddress => '1.1.1.1' }} + context "when only one port is provided" do + let(:params) do + { + :name => 'croy', + :ports => '18140' + } + end + + it { should contain_concat__fragment('croy_frontend_block').with( + 'order' => '15-croy-00', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => "\nfrontend croy\n bind 1.1.1.1:18140\n option tcplog\n" + ) } + end + context "when an array of ports is provided" do + let(:params) do + { + :name => 'apache', + :ipaddress => '23.23.23.23', + :ports => [ + '80', + '443' + ] + } + end + + it { should contain_concat__fragment('apache_frontend_block').with( + 'order' => '15-apache-00', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => "\nfrontend apache\n bind 23.23.23.23:80\n bind 23.23.23.23:443\n option tcplog\n" + ) } + end + context "when a comma-separated list of ports is provided" do + let(:params) do + { + :name => 'apache', + :ipaddress => '23.23.23.23', + :ports => '80,443' + } + end + + it { should contain_concat__fragment('apache_frontend_block').with( + 'order' => '15-apache-00', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => "\nfrontend apache\n bind 23.23.23.23:80\n bind 23.23.23.23:443\n option tcplog\n" + ) } + end +end diff --git a/haproxy/spec/defines/listen_spec.rb b/haproxy/spec/defines/listen_spec.rb new file mode 100644 index 000000000..ef46dcacd --- /dev/null +++ b/haproxy/spec/defines/listen_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'haproxy::listen' do + let(:title) { 'tyler' } + let(:facts) {{ :ipaddress => '1.1.1.1' }} + context "when only one port is provided" do + let(:params) do + { + :name => 'croy', + :ports => '18140' + } + end + + it { should contain_concat__fragment('croy_listen_block').with( + 'order' => '20-croy-00', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => "\nlisten croy\n bind 1.1.1.1:18140\n balance roundrobin\n option tcplog\n option ssl-hello-chk\n" + ) } + end + context "when an array of ports is provided" do + let(:params) do + { + :name => 'apache', + :ipaddress => '23.23.23.23', + :ports => [ + '80', + '443', + ] + } + end + + it { should contain_concat__fragment('apache_listen_block').with( + 'order' => '20-apache-00', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => "\nlisten apache\n bind 23.23.23.23:80\n bind 23.23.23.23:443\n balance roundrobin\n option tcplog\n option ssl-hello-chk\n" + ) } + end + context "when a comma-separated list of ports is provided" do + let(:params) do + { + :name => 'apache', + :ipaddress => '23.23.23.23', + :ports => '80,443' + } + end + + it { should contain_concat__fragment('apache_listen_block').with( + 'order' => '20-apache-00', + 'target' => '/etc/haproxy/haproxy.cfg', + 'content' => "\nlisten apache\n bind 23.23.23.23:80\n bind 23.23.23.23:443\n balance roundrobin\n option tcplog\n option ssl-hello-chk\n" + ) } + end +end diff --git a/haproxy/spec/spec.opts b/haproxy/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/haproxy/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/haproxy/spec/spec_helper.rb b/haproxy/spec/spec_helper.rb new file mode 100644 index 000000000..2c6f56649 --- /dev/null +++ b/haproxy/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/haproxy/templates/haproxy-base.cfg.erb b/haproxy/templates/haproxy-base.cfg.erb new file mode 100644 index 000000000..f25d5c344 --- /dev/null +++ b/haproxy/templates/haproxy-base.cfg.erb @@ -0,0 +1,21 @@ +global +<% @global_options.sort.each do |key,val| -%> +<% if val.is_a?(Array) -%> +<% val.each do |item| -%> + <%= key %> <%= item %> +<% end -%> +<% else -%> + <%= key %> <%= val %> +<% end -%> +<% end -%> + +defaults +<% @defaults_options.sort.each do |key,val| -%> +<% if val.is_a?(Array) -%> +<% val.each do |item| -%> + <%= key %> <%= item %> +<% end -%> +<% else -%> + <%= key %> <%= val %> +<% end -%> +<% end -%> diff --git a/haproxy/templates/haproxy_backend_block.erb b/haproxy/templates/haproxy_backend_block.erb new file mode 100644 index 000000000..95114cf46 --- /dev/null +++ b/haproxy/templates/haproxy_backend_block.erb @@ -0,0 +1,7 @@ + +backend <%= @name %> +<% @options.sort.each do |key, val| -%> +<% Array(val).each do |item| -%> + <%= key %> <%= item %> +<% end -%> +<% end -%> diff --git a/haproxy/templates/haproxy_balancermember.erb b/haproxy/templates/haproxy_balancermember.erb new file mode 100644 index 000000000..47c409c0b --- /dev/null +++ b/haproxy/templates/haproxy_balancermember.erb @@ -0,0 +1,3 @@ +<% Array(@ipaddresses).zip(Array(@server_names)).each do |ipaddress,host| -%> + server <%= host %> <%= ipaddress %>:<%= Array(@ports).collect {|x|x.split(',')}.flatten.join(",#{ipaddress}:") %> <%= if @define_cookies then "cookie " + host end %> <%= Array(@options).join(" ") %> +<% end -%> diff --git a/haproxy/templates/haproxy_frontend_block.erb b/haproxy/templates/haproxy_frontend_block.erb new file mode 100644 index 000000000..a1803ce4b --- /dev/null +++ b/haproxy/templates/haproxy_frontend_block.erb @@ -0,0 +1,13 @@ + +frontend <%= @name %> +<% Array(@ipaddress).uniq.each do |virtual_ip| (@ports.is_a?(Array) ? @ports : Array(@ports.split(","))).each do |port| -%> + bind <%= virtual_ip %>:<%= port %> +<% end end -%> +<% if @mode -%> + mode <%= @mode %> +<% end -%> +<% @options.sort.each do |key, val| -%> +<% Array(val).each do |item| -%> + <%= key %> <%= item %> +<% end -%> +<% end -%> diff --git a/haproxy/templates/haproxy_listen_block.erb b/haproxy/templates/haproxy_listen_block.erb new file mode 100644 index 000000000..62f295469 --- /dev/null +++ b/haproxy/templates/haproxy_listen_block.erb @@ -0,0 +1,13 @@ + +listen <%= @name %> +<% Array(@ipaddress).uniq.each do |virtual_ip| (@ports.is_a?(Array) ? @ports : Array(@ports.split(","))).each do |port| -%> + bind <%= virtual_ip %>:<%= port %> +<% end end -%> +<% if @mode -%> + mode <%= @mode %> +<% end -%> +<% @options.sort.each do |key, val| -%> +<% Array(val).each do |item| -%> + <%= key %> <%= item %> +<% end -%> +<% end -%> diff --git a/haproxy/tests/init.pp b/haproxy/tests/init.pp new file mode 100644 index 000000000..77590ac87 --- /dev/null +++ b/haproxy/tests/init.pp @@ -0,0 +1,69 @@ +# Declare haproxy base class with configuration options +class { 'haproxy': + enable => true, + global_options => { + 'log' => "${::ipaddress} local0", + 'chroot' => '/var/lib/haproxy', + 'pidfile' => '/var/run/haproxy.pid', + 'maxconn' => '4000', + 'user' => 'haproxy', + 'group' => 'haproxy', + 'daemon' => '', + 'stats' => 'socket /var/lib/haproxy/stats', + }, + defaults_options => { + 'log' => 'global', + 'stats' => 'enable', + 'option' => 'redispatch', + 'retries' => '3', + 'timeout' => [ + 'http-request 10s', + 'queue 1m', + 'connect 10s', + 'client 1m', + 'server 1m', + 'check 10s', + ], + 'maxconn' => '8000', + }, +} + +# Export a balancermember server, note that the listening_service parameter +# will/must correlate with an haproxy::listen defined resource type. +@@haproxy::balancermember { $fqdn: + order => '21', + listening_service => 'puppet00', + server_name => $::hostname, + balancer_ip => $::ipaddress, + balancer_port => '8140', + balancermember_options => 'check' +} + +# Declare a couple of Listening Services for haproxy.cfg +# Note that the balancermember server resources are being collected in +# the haproxy::config defined resource type with the following line: +# Haproxy::Balancermember <<| listening_service == $name |>> +haproxy::listen { 'puppet00': + order => '20', + ipaddress => $::ipaddress, + ports => '18140', + options => { + 'option' => [ + 'tcplog', + 'ssl-hello-chk', + ], + 'balance' => 'roundrobin', + }, +} +haproxy::listen { 'stats': + order => '30', + ipaddress => '', + ports => '9090', + options => { + 'mode' => 'http', + 'stats' => [ + 'uri /', + 'auth puppet:puppet' + ], + }, +} diff --git a/heat b/heat deleted file mode 160000 index e9e1ba05e..000000000 --- a/heat +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e9e1ba05e13948b8e0c7a72b1b68cefbedd2b40d diff --git a/heat/.fixtures.yml b/heat/.fixtures.yml new file mode 100644 index 000000000..40e23e9e1 --- /dev/null +++ b/heat/.fixtures.yml @@ -0,0 +1,12 @@ +fixtures: + repositories: + "inifile": "git://github.com/puppetlabs/puppetlabs-inifile" + "keystone": "git://github.com/stackforge/puppet-keystone.git" + "mysql": + repo: 'git://github.com/puppetlabs/puppetlabs-mysql.git' + ref: 'origin/2.2.x' + "nova": "git://github.com/stackforge/puppet-nova.git" + 'openstacklib': 'git://github.com/stackforge/puppet-openstacklib.git' + "stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib.git" + symlinks: + "heat": "#{source_dir}" diff --git a/heat/.gitignore b/heat/.gitignore new file mode 100644 index 000000000..1fc755c8f --- /dev/null +++ b/heat/.gitignore @@ -0,0 +1,5 @@ +Gemfile.lock +spec/fixtures/modules/* +spec/fixtures/manifests/site.pp +*.swp +pkg diff --git a/heat/.gitreview b/heat/.gitreview new file mode 100644 index 000000000..37bb1b287 --- /dev/null +++ b/heat/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=stackforge/puppet-heat.git diff --git a/heat/Gemfile b/heat/Gemfile new file mode 100644 index 000000000..0d35201b4 --- /dev/null +++ b/heat/Gemfile @@ -0,0 +1,15 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'puppetlabs_spec_helper', :require => false + gem 'puppet-lint', '~> 0.3.2' + gem 'rake', '10.1.1' +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/heat/LICENSE b/heat/LICENSE new file mode 100644 index 000000000..dd5b3a58a --- /dev/null +++ b/heat/LICENSE @@ -0,0 +1,174 @@ + 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. diff --git a/heat/Modulefile b/heat/Modulefile new file mode 100644 index 000000000..f3f6087c3 --- /dev/null +++ b/heat/Modulefile @@ -0,0 +1,13 @@ +name 'puppetlabs-heat' +version '4.0.0' +author 'eNovance and StackForge Contributors' +license 'Apache License 2.0' +summary 'Puppet module for OpenStack Heat' +description 'Installs and configures OpenStack Heat (Orchestration).' +project_page 'https://launchpad.net/puppet-heat' +source 'https://github.com/stackforge/puppet-heat' + +dependency 'puppetlabs/inifile', '>= 1.0.0 <2.0.0' +dependency 'puppetlabs/keystone', '>=4.0.0 <5.0.0' +dependency 'puppetlabs/stdlib', '>= 4.0.0 < 5.0.0' +dependency 'stackforge/openstacklib', '>=5.0.0' diff --git a/heat/README.md b/heat/README.md new file mode 100644 index 000000000..5e2d02a62 --- /dev/null +++ b/heat/README.md @@ -0,0 +1,115 @@ +puppet-heat +============= + +4.0.0 - 2014.1.0 - Icehouse + +#### Table of Contents + +1. [Overview - What is the heat module?](#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with heat](#setup) +4. [Implementation - An under-the-hood peek at what the module is doing](#implementation) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) +7. [Contributors - Those with commits](#contributors) +8. [Release Notes - Notes on the most recent updates to the module](#release-notes) + +Overview +-------- + +The heat module is part of [Stackforge](https://github.com/stackforge), an effort by the +OpenStack infrastructure team to provice continuous integration testing and code review for +OpenStack and OpenStack community projects not part of the core software. The module itself +is used to flexibly configure and manage the orchestration service for OpenStack + +Module Description +------------------ + +The heat module is an attempt to make Puppet capable of managing the entirety of heat. + +Setup +----- + +**What the heat module affects** + +* heat, the orchestration service for OpenStack + +### Installing heat + + example% puppet module install puppetlabs/heat + +### Beginning with heat + +Implementation +-------------- + +### puppet-heat + +heat is a combination of Puppet manifests and Ruby code to deliver configuration and +extra functionality through types and providers. + +Limitations +----------- + +None + +Development +----------- + +Developer documentation for the entire puppet-openstack project. + +* https://wiki.openstack.org/wiki/Puppet-openstack#Developer_documentation + +Contributors +------------ + +* https://github.com/stackforge/puppet-heat/graphs/contributors + +Release Notes +------------- + +**4.0.0** + +* Stable Icehouse release. +* Added SSL parameter for RabbitMQ. +* Added support for puppetlabs-mysql 2.2 and greater. +* Added option to define RabbitMQ queues as durable. +* Fixed outdated DB connection parameter. +* Fixed Keystone auth_uri parameter. + +**3.1.0** + +* Fixed postgresql connection string. +* Allow log_dir to be set to false to disable file logging. +* Added support for database idle timeout. +* Aligned Keystone auth_uri with other OpenStack services. +* Fixed the EC2 auth token settings. +* Fixed rabbit_virtual_host configuration. + +**3.0.0** + +* Initial release of the puppet-heat module. + +License +------- + +Apache License 2.0 + + Copyright 2012 eNovance and Authors + + 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. + +Contact +------- + +techs@enovance.com diff --git a/heat/Rakefile b/heat/Rakefile new file mode 100644 index 000000000..69432f021 --- /dev/null +++ b/heat/Rakefile @@ -0,0 +1,7 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings = true +PuppetLint.configuration.send('disable_names_containing_dash') +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_parameter_defaults') diff --git a/heat/examples/site.pp b/heat/examples/site.pp new file mode 100644 index 000000000..97087dc5d --- /dev/null +++ b/heat/examples/site.pp @@ -0,0 +1,29 @@ +node default { + Exec { + path => ['/usr/bin', '/bin', '/usr/sbin', '/sbin'] + } + + # First, install a mysql server + class { 'mysql::server': } + + # And create the database + class { 'heat::db::mysql': + password => 'heat', + } + + # Common class + class { 'heat': + # The keystone_password parameter is mandatory + keystone_password => 'password', + sql_connection => 'mysql://heat:heat@localhost/heat' + } + + # Install heat-engine + class { 'heat::engine': + auth_encryption_key => 'whatever-key-you-like', + } + + # Install the heat-api service + class { 'heat::api': } + +} diff --git a/heat/lib/puppet/provider/heat_config/ini_setting.rb b/heat/lib/puppet/provider/heat_config/ini_setting.rb new file mode 100644 index 000000000..2c3ab5fff --- /dev/null +++ b/heat/lib/puppet/provider/heat_config/ini_setting.rb @@ -0,0 +1,22 @@ +Puppet::Type.type(:heat_config).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def file_path + '/etc/heat/heat.conf' + end + +end diff --git a/heat/lib/puppet/type/heat_config.rb b/heat/lib/puppet/type/heat_config.rb new file mode 100644 index 000000000..adcc5ce93 --- /dev/null +++ b/heat/lib/puppet/type/heat_config.rb @@ -0,0 +1,43 @@ +Puppet::Type.newtype(:heat_config) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from heat.conf' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end + +end diff --git a/heat/manifests/api-cfn.pp b/heat/manifests/api-cfn.pp new file mode 100644 index 000000000..6e30387b8 --- /dev/null +++ b/heat/manifests/api-cfn.pp @@ -0,0 +1,36 @@ +# Installs & configure the heat CloudFormation API service +# +class heat::api-cfn ( + $enabled = true, + $keystone_host = '127.0.0.1', + $keystone_port = '35357', + $keystone_protocol = 'http', + $keystone_user = 'heat', + $keystone_tenant = 'services', + $keystone_password = false, + $keystone_ec2_uri = 'http://127.0.0.1:5000/v2.0/ec2tokens', + $auth_uri = 'http://127.0.0.1:5000/v2.0', + $bind_host = '0.0.0.0', + $bind_port = '8000', + $verbose = false, + $debug = false, +) { + + warning('heat::api-cfn is deprecated. Use heat::api_cfn instead.') + + class { 'heat::api_cfn': + enabled => $enabled, + keystone_host => $keystone_host, + keystone_port => $keystone_port, + keystone_protocol => $keystone_protocol, + keystone_user => $keystone_user, + keystone_tenant => $keystone_tenant, + keystone_password => $keystone_password, + keystone_ec2_uri => $keystone_ec2_uri, + auth_uri => $auth_uri, + bind_host => $bind_host, + bind_port => $bind_port, + verbose => $verbose, + debug => $debug, + } +} diff --git a/heat/manifests/api-cloudwatch.pp b/heat/manifests/api-cloudwatch.pp new file mode 100644 index 000000000..67657b16b --- /dev/null +++ b/heat/manifests/api-cloudwatch.pp @@ -0,0 +1,36 @@ +# Installs & configure the heat CloudWatch API service +# +class heat::api-cloudwatch ( + $enabled = true, + $keystone_host = '127.0.0.1', + $keystone_port = '35357', + $keystone_protocol = 'http', + $keystone_user = 'heat', + $keystone_tenant = 'services', + $keystone_password = false, + $keystone_ec2_uri = 'http://127.0.0.1:5000/v2.0/ec2tokens', + $auth_uri = 'http://127.0.0.1:5000/v2.0', + $bind_host = '0.0.0.0', + $bind_port = '8003', + $verbose = false, + $debug = false, +) { + + warning('heat::api-cloudwatch is deprecated. Use heat::api_cloudwatch instead.') + + class { 'heat::api_cloudwatch': + enabled => $enabled, + keystone_host => $keystone_host, + keystone_port => $keystone_port, + keystone_protocol => $keystone_protocol, + keystone_user => $keystone_user, + keystone_tenant => $keystone_tenant, + keystone_password => $keystone_password, + keystone_ec2_uri => $keystone_ec2_uri, + auth_uri => $auth_uri, + bind_host => $bind_host, + bind_port => $bind_port, + verbose => $verbose, + debug => $debug, + } +} diff --git a/heat/manifests/api.pp b/heat/manifests/api.pp new file mode 100644 index 000000000..4a3dcfac2 --- /dev/null +++ b/heat/manifests/api.pp @@ -0,0 +1,83 @@ +# Installs & configure the heat API service +# +# == Parameters +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +class heat::api ( + $manage_service = true, + $enabled = true, + $bind_host = '0.0.0.0', + $bind_port = '8004', + $workers = '0', + $use_ssl = false, + $cert_file = false, + $key_file = false, +) { + + include heat + include heat::params + + Heat_config<||> ~> Service['heat-api'] + + Package['heat-api'] -> Heat_config<||> + Package['heat-api'] -> Service['heat-api'] + + if $use_ssl { + if !$cert_file { + fail('The cert_file parameter is required when use_ssl is set to true') + } + if !$key_file { + fail('The key_file parameter is required when use_ssl is set to true') + } + } + + package { 'heat-api': + ensure => installed, + name => $::heat::params::api_package_name, + } + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + service { 'heat-api': + ensure => $service_ensure, + name => $::heat::params::api_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + require => [Package['heat-common'], + Package['heat-api']], + subscribe => Exec['heat-dbsync'], + } + + heat_config { + 'heat_api/bind_host' : value => $bind_host; + 'heat_api/bind_port' : value => $bind_port; + 'heat_api/workers' : value => $workers; + } + + # SSL Options + if $use_ssl { + heat_config { + 'heat_api/cert_file' : value => $cert_file; + 'heat_api/key_file' : value => $key_file; + } + } else { + heat_config { + 'heat_api/cert_file' : ensure => absent; + 'heat_api/key_file' : ensure => absent; + } + } + +} diff --git a/heat/manifests/api_cfn.pp b/heat/manifests/api_cfn.pp new file mode 100644 index 000000000..7e5940e9f --- /dev/null +++ b/heat/manifests/api_cfn.pp @@ -0,0 +1,83 @@ +# Installs & configure the heat CloudFormation API service +# +# == Parameters +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +class heat::api_cfn ( + $manage_service = true, + $enabled = true, + $bind_host = '0.0.0.0', + $bind_port = '8000', + $workers = '0', + $use_ssl = false, + $cert_file = false, + $key_file = false, +) { + + include heat + include heat::params + + Heat_config<||> ~> Service['heat-api-cfn'] + + Package['heat-api-cfn'] -> Heat_config<||> + Package['heat-api-cfn'] -> Service['heat-api-cfn'] + + if $use_ssl { + if !$cert_file { + fail('The cert_file parameter is required when use_ssl is set to true') + } + if !$key_file { + fail('The key_file parameter is required when use_ssl is set to true') + } + } + + package { 'heat-api-cfn': + ensure => installed, + name => $::heat::params::api_cfn_package_name, + } + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + Package['heat-common'] -> Service['heat-api-cfn'] + + service { 'heat-api-cfn': + ensure => $service_ensure, + name => $::heat::params::api_cfn_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + subscribe => Exec['heat-dbsync'], + } + + heat_config { + 'heat_api_cfn/bind_host' : value => $bind_host; + 'heat_api_cfn/bind_port' : value => $bind_port; + 'heat_api_cfn/workers' : value => $workers; + } + + # SSL Options + if $use_ssl { + heat_config { + 'heat_api_cfn/cert_file' : value => $cert_file; + 'heat_api_cfn/key_file' : value => $key_file; + } + } else { + heat_config { + 'heat_api_cfn/cert_file' : ensure => absent; + 'heat_api_cfn/key_file' : ensure => absent; + } + } + +} diff --git a/heat/manifests/api_cloudwatch.pp b/heat/manifests/api_cloudwatch.pp new file mode 100644 index 000000000..280bc29f6 --- /dev/null +++ b/heat/manifests/api_cloudwatch.pp @@ -0,0 +1,84 @@ +# Installs & configure the heat CloudWatch API service +# +# == Parameters +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +class heat::api_cloudwatch ( + $manage_service = true, + $enabled = true, + $bind_host = '0.0.0.0', + $bind_port = '8003', + $workers = '0', + $use_ssl = false, + $cert_file = false, + $key_file = false, +) { + + include heat + include heat::params + + Heat_config<||> ~> Service['heat-api-cloudwatch'] + + Package['heat-api-cloudwatch'] -> Heat_config<||> + Package['heat-api-cloudwatch'] -> Service['heat-api-cloudwatch'] + + if $use_ssl { + if !$cert_file { + fail('The cert_file parameter is required when use_ssl is set to true') + } + if !$key_file { + fail('The key_file parameter is required when use_ssl is set to true') + } + } + + package { 'heat-api-cloudwatch': + ensure => installed, + name => $::heat::params::api_cloudwatch_package_name, + } + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + + Package['heat-common'] -> Service['heat-api-cloudwatch'] + + service { 'heat-api-cloudwatch': + ensure => $service_ensure, + name => $::heat::params::api_cloudwatch_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + subscribe => Exec['heat-dbsync'], + } + + heat_config { + 'heat_api_cloudwatch/bind_host' : value => $bind_host; + 'heat_api_cloudwatch/bind_port' : value => $bind_port; + 'heat_api_cloudwatch/workers' : value => $workers; + } + + # SSL Options + if $use_ssl { + heat_config { + 'heat_api_cloudwatch/cert_file' : value => $cert_file; + 'heat_api_cloudwatch/key_file' : value => $key_file; + } + } else { + heat_config { + 'heat_api_cloudwatch/cert_file' : ensure => absent; + 'heat_api_cloudwatch/key_file' : ensure => absent; + } + } + +} diff --git a/heat/manifests/client.pp b/heat/manifests/client.pp new file mode 100644 index 000000000..86fc4d889 --- /dev/null +++ b/heat/manifests/client.pp @@ -0,0 +1,19 @@ +# +# Installs the heat python library. +# +# == parameters +# [*ensure*] +# ensure state for pachage. +# +class heat::client ( + $ensure = 'present' +) { + + include heat::params + + package { 'python-heatclient': + ensure => $ensure, + name => $::heat::params::client_package_name, + } + +} diff --git a/heat/manifests/db/mysql.pp b/heat/manifests/db/mysql.pp new file mode 100644 index 000000000..f477b5938 --- /dev/null +++ b/heat/manifests/db/mysql.pp @@ -0,0 +1,62 @@ +# The heat::db::mysql class creates a MySQL database for heat. +# It must be used on the MySQL server +# +# == Parameters +# +# [*password*] +# password to connect to the database. Mandatory. +# +# [*dbname*] +# name of the database. Optional. Defaults to heat. +# +# [*user*] +# user to connect to the database. Optional. Defaults to heat. +# +# [*host*] +# the default source host user is allowed to connect from. +# Optional. Defaults to 'localhost' +# +# [*allowed_hosts*] +# other hosts the user is allowd to connect from. +# Optional. Defaults to undef. +# +# [*charset*] +# the database charset. Optional. Defaults to 'utf8' +# +# [*collate*] +# the database collate. Optional. Only used with mysql modules +# >= 2.2 +# Defaults to 'utf8_unicode_ci' +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +class heat::db::mysql( + $password = false, + $dbname = 'heat', + $user = 'heat', + $host = '127.0.0.1', + $allowed_hosts = undef, + $charset = 'utf8', + $collate = 'utf8_unicode_ci', + $mysql_module = undef +) { + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + validate_string($password) + + ::openstacklib::db::mysql { 'heat': + user => $user, + password_hash => mysql_password($password), + dbname => $dbname, + host => $host, + charset => $charset, + collate => $collate, + allowed_hosts => $allowed_hosts, + } + + ::Openstacklib::Db::Mysql['heat'] ~> Exec<| title == 'heat-dbsync' |> +} diff --git a/heat/manifests/engine.pp b/heat/manifests/engine.pp new file mode 100644 index 000000000..2238d6cfe --- /dev/null +++ b/heat/manifests/engine.pp @@ -0,0 +1,89 @@ +# Class heat::engine +# +# Installs & configure the heat engine service +# +# == parameters +# [*enabled*] +# (optional) Should the service be enabled. +# Defaults to true +# +# [*manage_service*] +# (optional) Whether the service should be managed by Puppet. +# Defaults to true. +# +# [*heat_stack_user_role*] +# (optional) Keystone role for heat template-defined users +# Defaults to 'heat_stack_user' +# +# [*heat_metadata_server_url*] +# (optional) URL of the Heat metadata server +# Defaults to 'http://127.0.0.1:8000' +# +# [*heat_waitcondition_server_url*] +# (optional) URL of the Heat waitcondition server +# Defaults to 'http://127.0.0.1:8000/v1/waitcondition' +# +# [*heat_watch_server_url*] +# (optional) URL of the Heat cloudwatch server +# Defaults to 'http://127.0.0.1:8003' +# +# [*auth_encryption_key*] +# (required) Encryption key used for authentication info in database +# +# [*engine_life_check_timeout*] +# (optional) RPC timeout (in seconds) for the engine liveness check that is +# used for stack locking +# Defaults to '2' +# + +class heat::engine ( + $auth_encryption_key, + $manage_service = true, + $enabled = true, + $heat_stack_user_role = 'heat_stack_user', + $heat_metadata_server_url = 'http://127.0.0.1:8000', + $heat_waitcondition_server_url = 'http://127.0.0.1:8000/v1/waitcondition', + $heat_watch_server_url = 'http://127.0.0.1:8003', + $engine_life_check_timeout = '2' +) { + + include heat::params + + Heat_config<||> ~> Service['heat-engine'] + + Package['heat-engine'] -> Heat_config<||> + Package['heat-engine'] -> Service['heat-engine'] + package { 'heat-engine': + ensure => installed, + name => $::heat::params::engine_package_name, + } + + if $manage_service { + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + } + + service { 'heat-engine': + ensure => $service_ensure, + name => $::heat::params::engine_service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + require => [ File['/etc/heat/heat.conf'], + Package['heat-common'], + Package['heat-engine']], + subscribe => Exec['heat-dbsync'], + } + + heat_config { + 'DEFAULT/auth_encryption_key' : value => $auth_encryption_key; + 'DEFAULT/heat_stack_user_role' : value => $heat_stack_user_role; + 'DEFAULT/heat_metadata_server_url' : value => $heat_metadata_server_url; + 'DEFAULT/heat_waitcondition_server_url': value => $heat_waitcondition_server_url; + 'DEFAULT/heat_watch_server_url' : value => $heat_watch_server_url; + 'DEFAULT/engine_life_check_timeout' : value => $engine_life_check_timeout; + } +} diff --git a/heat/manifests/init.pp b/heat/manifests/init.pp new file mode 100644 index 000000000..a48218eac --- /dev/null +++ b/heat/manifests/init.pp @@ -0,0 +1,371 @@ +# Class heat +# +# heat base package & configuration +# +# == parameters +# [*package_ensure*] +# ensure state for package. Optional. Defaults to 'present' +# [*verbose*] +# should the daemons log verbose messages. Optional. Defaults to 'False' +# [*debug*] +# should the daemons log debug messages. Optional. Defaults to 'False' +# +# [*log_dir*] +# (optional) Directory where logs should be stored. +# If set to boolean false, it will not log to any directory. +# Defaults to '/var/log/heat'. +# +# [*rabbit_host*] +# ip or hostname of the rabbit server. Optional. Defaults to '127.0.0.1' +# [*rabbit_port*] +# port of the rabbit server. Optional. Defaults to 5672. +# [*rabbit_hosts*] +# array of host:port (used with HA queues). Optional. Defaults to undef. +# If defined, will remove rabbit_host & rabbit_port parameters from config +# [*rabbit_userid*] +# user to connect to the rabbit server. Optional. Defaults to 'guest' +# [*rabbit_password*] +# password to connect to the rabbit_server. Optional. Defaults to empty. +# [*rabbit_virtual_host*] +# virtual_host to use. Optional. Defaults to '/' +# [*rabbit_use_ssl*] +# (optional) Connect over SSL for RabbitMQ +# Defaults to false +# [*kombu_ssl_ca_certs*] +# (optional) SSL certification authority file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_certfile*] +# (optional) SSL cert file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_keyfile*] +# (optional) SSL key file (valid only if SSL enabled). +# Defaults to undef +# [*kombu_ssl_version*] +# (optional) SSL version to use (valid only if SSL enabled). +# Valid values are TLSv1, SSLv23 and SSLv3. SSLv2 may be +# available on some distributions. +# Defaults to 'SSLv3' +# [*amqp_durable_queues*] +# Use durable queues in amqp. Defaults to false +# +# (keystone authentication options) +# [*auth_uri*] +# Specifies the Authentication URI for Heat to use. Located in heat.conf +# Optional. Defaults to false, which uses: +# "${keystone_protocol}://${keystone_host}:5000/v2.0" +# [*keystone_host*] +# [*keystone_port*] +# [*keystone_protocol*] +# [*keystone_user*] +# [*keystone_tenant*] +# [*keystone_password*] +# [*keystone_ec2_uri*] +# +# (optional) various QPID options +# [*qpid_hostname*] +# [*qpid_port*] +# [*qpid_username*] +# [*qpid_password*] +# [*qpid_heartbeat*] +# [*qpid_protocol*] +# [*qpid_tcp_nodelay*] +# [*qpid_reconnect*] +# [*qpid_reconnect_timeout*] +# [*qpid_reconnect_limit*] +# [*qpid_reconnect_interval*] +# [*qpid_reconnect_interval_min*] +# [*qpid_reconnect_interval_max*] +# +# [*database_idle_timeout*] +# (optional) Timeout before idle db connections are reaped. +# Defaults to 3600 +# +# [*use_syslog*] +# (optional) Use syslog for logging +# Defaults to false +# +# [*log_facility*] +# (optional) Syslog facility to receive log lines +# Defaults to LOG_USER +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +class heat( + $auth_uri = false, + $package_ensure = 'present', + $verbose = false, + $debug = false, + $log_dir = '/var/log/heat', + $keystone_host = '127.0.0.1', + $keystone_port = '35357', + $keystone_protocol = 'http', + $keystone_user = 'heat', + $keystone_tenant = 'services', + $keystone_password = false, + $keystone_ec2_uri = 'http://127.0.0.1:5000/v2.0/ec2tokens', + $rpc_backend = 'heat.openstack.common.rpc.impl_kombu', + $rabbit_host = '127.0.0.1', + $rabbit_port = 5672, + $rabbit_hosts = undef, + $rabbit_userid = 'guest', + $rabbit_password = '', + $rabbit_virtual_host = '/', + $rabbit_use_ssl = false, + $kombu_ssl_ca_certs = undef, + $kombu_ssl_certfile = undef, + $kombu_ssl_keyfile = undef, + $kombu_ssl_version = 'SSLv3', + $amqp_durable_queues = false, + $qpid_hostname = 'localhost', + $qpid_port = 5672, + $qpid_username = 'guest', + $qpid_password = 'guest', + $qpid_heartbeat = 60, + $qpid_protocol = 'tcp', + $qpid_tcp_nodelay = true, + $qpid_reconnect = true, + $qpid_reconnect_timeout = 0, + $qpid_reconnect_limit = 0, + $qpid_reconnect_interval_min = 0, + $qpid_reconnect_interval_max = 0, + $qpid_reconnect_interval = 0, + $sql_connection = false, + $database_idle_timeout = 3600, + $use_syslog = false, + $log_facility = 'LOG_USER', + #Deprecated parameters + $mysql_module = undef, +) { + + include heat::params + + if $kombu_ssl_ca_certs and !$rabbit_use_ssl { + fail('The kombu_ssl_ca_certs parameter requires rabbit_use_ssl to be set to true') + } + if $kombu_ssl_certfile and !$rabbit_use_ssl { + fail('The kombu_ssl_certfile parameter requires rabbit_use_ssl to be set to true') + } + if $kombu_ssl_keyfile and !$rabbit_use_ssl { + fail('The kombu_ssl_keyfile parameter requires rabbit_use_ssl to be set to true') + } + if ($kombu_ssl_certfile and !$kombu_ssl_keyfile) or ($kombu_ssl_keyfile and !$kombu_ssl_certfile) { + fail('The kombu_ssl_certfile and kombu_ssl_keyfile parameters must be used together') + } + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + File { + require => Package['heat-common'], + } + + group { 'heat': + name => 'heat', + require => Package['heat-common'], + } + + user { 'heat': + name => 'heat', + gid => 'heat', + groups => ['heat'], + system => true, + require => Package['heat-common'], + } + + file { '/etc/heat/': + ensure => directory, + owner => 'heat', + group => 'heat', + mode => '0750', + } + + file { '/etc/heat/heat.conf': + owner => 'heat', + group => 'heat', + mode => '0640', + } + + package { 'heat-common': + ensure => $package_ensure, + name => $::heat::params::common_package_name, + } + + Package['heat-common'] -> Heat_config<||> + + if $rpc_backend == 'heat.openstack.common.rpc.impl_kombu' { + + if $rabbit_hosts { + heat_config { 'DEFAULT/rabbit_host': ensure => absent } + heat_config { 'DEFAULT/rabbit_port': ensure => absent } + heat_config { 'DEFAULT/rabbit_hosts': + value => join($rabbit_hosts, ',') + } + } else { + heat_config { 'DEFAULT/rabbit_host': value => $rabbit_host } + heat_config { 'DEFAULT/rabbit_port': value => $rabbit_port } + heat_config { 'DEFAULT/rabbit_hosts': + value => "${rabbit_host}:${rabbit_port}" + } + } + + if size($rabbit_hosts) > 1 { + heat_config { 'DEFAULT/rabbit_ha_queues': value => true } + } else { + heat_config { 'DEFAULT/rabbit_ha_queues': value => false } + } + + heat_config { + 'DEFAULT/rabbit_userid' : value => $rabbit_userid; + 'DEFAULT/rabbit_password' : value => $rabbit_password, secret => true; + 'DEFAULT/rabbit_virtual_host' : value => $rabbit_virtual_host; + 'DEFAULT/rabbit_use_ssl' : value => $rabbit_use_ssl; + 'DEFAULT/amqp_durable_queues' : value => $amqp_durable_queues; + } + + if $rabbit_use_ssl { + + if $kombu_ssl_ca_certs { + heat_config { 'DEFAULT/kombu_ssl_ca_certs': value => $kombu_ssl_ca_certs; } + } else { + heat_config { 'DEFAULT/kombu_ssl_ca_certs': ensure => absent; } + } + + if $kombu_ssl_certfile or $kombu_ssl_keyfile { + heat_config { + 'DEFAULT/kombu_ssl_certfile': value => $kombu_ssl_certfile; + 'DEFAULT/kombu_ssl_keyfile': value => $kombu_ssl_keyfile; + } + } else { + heat_config { + 'DEFAULT/kombu_ssl_certfile': ensure => absent; + 'DEFAULT/kombu_ssl_keyfile': ensure => absent; + } + } + + if $kombu_ssl_version { + heat_config { 'DEFAULT/kombu_ssl_version': value => $kombu_ssl_version; } + } else { + heat_config { 'DEFAULT/kombu_ssl_version': ensure => absent; } + } + + } else { + heat_config { + 'DEFAULT/kombu_ssl_version': ensure => absent; + 'DEFAULT/kombu_ssl_ca_certs': ensure => absent; + 'DEFAULT/kombu_ssl_certfile': ensure => absent; + 'DEFAULT/kombu_ssl_keyfile': ensure => absent; + } + } + + } + + if $rpc_backend == 'heat.openstack.common.rpc.impl_qpid' { + + heat_config { + 'DEFAULT/qpid_hostname' : value => $qpid_hostname; + 'DEFAULT/qpid_port' : value => $qpid_port; + 'DEFAULT/qpid_username' : value => $qpid_username; + 'DEFAULT/qpid_password' : value => $qpid_password, secret => true; + 'DEFAULT/qpid_heartbeat' : value => $qpid_heartbeat; + 'DEFAULT/qpid_protocol' : value => $qpid_protocol; + 'DEFAULT/qpid_tcp_nodelay' : value => $qpid_tcp_nodelay; + 'DEFAULT/qpid_reconnect' : value => $qpid_reconnect; + 'DEFAULT/qpid_reconnect_timeout' : value => $qpid_reconnect_timeout; + 'DEFAULT/qpid_reconnect_limit' : value => $qpid_reconnect_limit; + 'DEFAULT/qpid_reconnect_interval_min' : value => $qpid_reconnect_interval_min; + 'DEFAULT/qpid_reconnect_interval_max' : value => $qpid_reconnect_interval_max; + 'DEFAULT/qpid_reconnect_interval' : value => $qpid_reconnect_interval; + 'DEFAULT/amqp_durable_queues' : value => $amqp_durable_queues; + } + + } + + if $auth_uri { + heat_config { 'keystone_authtoken/auth_uri': value => $auth_uri; } + } else { + heat_config { 'keystone_authtoken/auth_uri': value => "${keystone_protocol}://${keystone_host}:5000/v2.0"; } + } + + heat_config { + 'DEFAULT/rpc_backend' : value => $rpc_backend; + 'DEFAULT/debug' : value => $debug; + 'DEFAULT/verbose' : value => $verbose; + 'ec2authtoken/auth_uri' : value => $keystone_ec2_uri; + 'keystone_authtoken/auth_host' : value => $keystone_host; + 'keystone_authtoken/auth_port' : value => $keystone_port; + 'keystone_authtoken/auth_protocol' : value => $keystone_protocol; + 'keystone_authtoken/admin_tenant_name' : value => $keystone_tenant; + 'keystone_authtoken/admin_user' : value => $keystone_user; + 'keystone_authtoken/admin_password' : value => $keystone_password; + } + + # Log configuration + if $log_dir { + heat_config { + 'DEFAULT/log_dir' : value => $log_dir; + } + } else { + heat_config { + 'DEFAULT/log_dir' : ensure => absent; + } + } + + if $sql_connection { + + validate_re($sql_connection, + '(sqlite|mysql|postgresql):\/\/(\S+:\S+@\S+\/\S+)?') + + case $sql_connection { + /^mysql:\/\//: { + $backend_package = false + require mysql::bindings + require mysql::bindings::python + } + /^postgresql:\/\//: { + $backend_package = 'python-psycopg2' + } + /^sqlite:\/\//: { + $backend_package = 'python-pysqlite2' + } + default: { + fail('Unsupported backend configured') + } + } + + if $backend_package and !defined(Package[$backend_package]) { + package {'heat-backend-package': + ensure => present, + name => $backend_package, + } + } + + heat_config { + 'database/connection': value => $sql_connection, secret => true; + 'database/idle_timeout': value => $database_idle_timeout; + } + + Heat_config['database/connection'] ~> Exec['heat-dbsync'] + + exec { 'heat-dbsync': + command => $::heat::params::dbsync_command, + path => '/usr/bin', + user => 'heat', + refreshonly => true, + logoutput => on_failure, + } + } + + # Syslog configuration + if $use_syslog { + heat_config { + 'DEFAULT/use_syslog': value => true; + 'DEFAULT/syslog_log_facility': value => $log_facility; + } + } else { + heat_config { + 'DEFAULT/use_syslog': value => false; + } + } + +} diff --git a/heat/manifests/keystone/auth.pp b/heat/manifests/keystone/auth.pp new file mode 100644 index 000000000..3302cca81 --- /dev/null +++ b/heat/manifests/keystone/auth.pp @@ -0,0 +1,120 @@ +# == Class: heat::heat::auth +# +# Configures heat user, service and endpoint in Keystone. +# +# === Parameters +# +# [*password*] +# Password for heat user. Required. +# +# [*email*] +# Email for heat user. Optional. Defaults to 'heat@localhost'. +# +# [*auth_name*] +# Username for heat service. Optional. Defaults to 'heat'. +# +# [*configure_endpoint*] +# Should heat endpoint be configured? Optional. Defaults to 'true'. +# +# [*configure_user*] +# Whether to create the service user. Defaults to 'true'. +# +# [*configure_user_role*] +# Whether to configure the admin role for teh service user. Defaults to 'true'. +# +# [*service_name*] +# Name of the service. Options. Defaults to the value of auth_name. +# +# [*service_type*] +# Type of service. Optional. Defaults to 'orchestration'. +# +# [*public_address*] +# Public address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*admin_address*] +# Admin address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*internal_address*] +# Internal address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*version*] +# Version of API to use. Optional. Defaults to 'v1' +# +# [*port*] +# Port for endpoint. Optional. Defaults to '8004'. +# +# [*region*] +# Region for endpoint. Optional. Defaults to 'RegionOne'. +# +# [*tenant*] +# Tenant for heat user. Optional. Defaults to 'services'. +# +# [*protocol*] +# Protocol for public endpoint. Optional. Defaults to 'http'. +# +class heat::keystone::auth ( + $password = false, + $email = 'heat@localhost', + $auth_name = 'heat', + $service_name = undef, + $service_type = 'orchestration', + $public_address = '127.0.0.1', + $admin_address = '127.0.0.1', + $internal_address = '127.0.0.1', + $port = '8004', + $version = 'v1', + $region = 'RegionOne', + $tenant = 'services', + $public_protocol = 'http', + $admin_protocol = 'http', + $internal_protocol = 'http', + $configure_endpoint = true, + $configure_user = true, + $configure_user_role = true, +) { + + validate_string($password) + + if $service_name == undef { + $real_service_name = $auth_name + } else { + $real_service_name = $service_name + } + + if $configure_user { + keystone_user { $auth_name: + ensure => present, + password => $password, + email => $email, + tenant => $tenant, + } + } + + if $configure_user_role { + Keystone_user_role["${auth_name}@${tenant}"] ~> + Service <| name == 'heat-api' |> + + keystone_user_role { "${auth_name}@${tenant}": + ensure => present, + roles => ['admin'], + } + } + + keystone_role { 'heat_stack_user': + ensure => present, + } + + keystone_service { $real_service_name: + ensure => present, + type => $service_type, + description => 'Openstack Orchestration Service', + } + if $configure_endpoint { + keystone_endpoint { "${region}/${real_service_name}": + ensure => present, + public_url => "${public_protocol}://${public_address}:${port}/${version}/%(tenant_id)s", + admin_url => "${admin_protocol}://${admin_address}:${port}/${version}/%(tenant_id)s", + internal_url => "${internal_protocol}://${internal_address}:${port}/${version}/%(tenant_id)s", + } + } +} diff --git a/heat/manifests/keystone/auth_cfn.pp b/heat/manifests/keystone/auth_cfn.pp new file mode 100644 index 000000000..36c0fa083 --- /dev/null +++ b/heat/manifests/keystone/auth_cfn.pp @@ -0,0 +1,116 @@ +# == Class: heat::heat::auth_cfn +# +# Configures heat-api-cfn user, service and endpoint in Keystone. +# +# === Parameters +# +# [*password*] +# Password for heat-cfn user. Required. +# +# [*email*] +# Email for heat-cfn user. Optional. Defaults to 'heat@localhost'. +# +# [*auth_name*] +# Username for heat-cfn service. Optional. Defaults to 'heat'. +# +# [*configure_endpoint*] +# Should heat-cfn endpoint be configured? Optional. Defaults to 'true'. +# +# [*configure_user*] +# Whether to create the service user. Defaults to 'true'. +# +# [*configure_user_role*] +# Whether to configure the admin role for the service user. Defaults to 'true'. +# +# [*service_name*] +# Name of the service. Optional. Defaults to the value of auth_name. +# +# [*service_type*] +# Type of service. Optional. Defaults to 'cloudformation'. +# +# [*public_address*] +# Public address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*admin_address*] +# Admin address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*internal_address*] +# Internal address for endpoint. Optional. Defaults to '127.0.0.1'. +# +# [*port*] +# Port for endpoint. Optional. Defaults to '8000'. +# +# [*version*] +# Version for API. Optional. Defaults to 'v1' + +# [*region*] +# Region for endpoint. Optional. Defaults to 'RegionOne'. +# +# [*tenant*] +# Tenant for heat-cfn user. Optional. Defaults to 'services'. +# +# [*protocol*] +# Protocol for public endpoint. Optional. Defaults to 'http'. +# +class heat::keystone::auth_cfn ( + $password = false, + $email = 'heat-cfn@localhost', + $auth_name = 'heat-cfn', + $service_name = undef, + $service_type = 'cloudformation', + $public_address = '127.0.0.1', + $admin_address = '127.0.0.1', + $internal_address = '127.0.0.1', + $port = '8000', + $version = 'v1', + $region = 'RegionOne', + $tenant = 'services', + $public_protocol = 'http', + $admin_protocol = 'http', + $internal_protocol = 'http', + $configure_endpoint = true, + $configure_user = true, + $configure_user_role = true, +) { + + validate_string($password) + + if $service_name == undef { + $real_service_name = $auth_name + } else { + $real_service_name = $service_name + } + + if $configure_user { + keystone_user { $auth_name: + ensure => present, + password => $password, + email => $email, + tenant => $tenant, + } + } + + if $configure_user_role { + Keystone_user_role["${auth_name}@${tenant}"] ~> + Service <| name == 'heat-api-cfn' |> + + keystone_user_role { "${auth_name}@${tenant}": + ensure => present, + roles => ['admin'], + } + } + + keystone_service { $real_service_name: + ensure => present, + type => $service_type, + description => 'Openstack Cloudformation Service', + } + if $configure_endpoint { + keystone_endpoint { "${region}/${real_service_name}": + ensure => present, + public_url => "${public_protocol}://${public_address}:${port}/${version}/", + admin_url => "${admin_protocol}://${admin_address}:${port}/${version}/", + internal_url => "${internal_protocol}://${internal_address}:${port}/${version}/", + } + } +} diff --git a/heat/manifests/logging.pp b/heat/manifests/logging.pp new file mode 100644 index 000000000..d18b5c2a7 --- /dev/null +++ b/heat/manifests/logging.pp @@ -0,0 +1,208 @@ +# Class heat::logging +# +# heat extended logging configuration +# +# == parameters +# +# [*logging_context_format_string*] +# (optional) Format string to use for log messages with context. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s\ +# [%(request_id)s %(user_identity)s] %(instance)s%(message)s' +# +# [*logging_default_format_string*] +# (optional) Format string to use for log messages without context. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s\ +# [-] %(instance)s%(message)s' +# +# [*logging_debug_format_suffix*] +# (optional) Formatted data to append to log format when level is DEBUG. +# Defaults to undef. +# Example: '%(funcName)s %(pathname)s:%(lineno)d' +# +# [*logging_exception_prefix*] +# (optional) Prefix each line of exception output with this format. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s' +# +# [*log_config_append*] +# The name of an additional logging configuration file. +# Defaults to undef. +# See https://docs.python.org/2/howto/logging.html +# +# [*default_log_levels*] +# (optional) Hash of logger (keys) and level (values) pairs. +# Defaults to undef. +# Example: +# { 'amqp' => 'WARN', 'amqplib' => 'WARN', 'boto' => 'WARN', +# 'qpid' => 'WARN', 'sqlalchemy' => 'WARN', 'suds' => 'INFO', +# 'iso8601' => 'WARN', +# 'requests.packages.urllib3.connectionpool' => 'WARN' } +# +# [*publish_errors*] +# (optional) Publish error events (boolean value). +# Defaults to undef (false if unconfigured). +# +# [*fatal_deprecations*] +# (optional) Make deprecations fatal (boolean value) +# Defaults to undef (false if unconfigured). +# +# [*instance_format*] +# (optional) If an instance is passed with the log message, format it +# like this (string value). +# Defaults to undef. +# Example: '[instance: %(uuid)s] ' +# +# [*instance_uuid_format*] +# (optional) If an instance UUID is passed with the log message, format +# it like this (string value). +# Defaults to undef. +# Example: instance_uuid_format='[instance: %(uuid)s] ' + +# [*log_date_format*] +# (optional) Format string for %%(asctime)s in log records. +# Defaults to undef. +# Example: 'Y-%m-%d %H:%M:%S' + +class heat::logging( + $logging_context_format_string = undef, + $logging_default_format_string = undef, + $logging_debug_format_suffix = undef, + $logging_exception_prefix = undef, + $log_config_append = undef, + $default_log_levels = undef, + $publish_errors = undef, + $fatal_deprecations = undef, + $instance_format = undef, + $instance_uuid_format = undef, + $log_date_format = undef, +) { + + if $logging_context_format_string { + heat_config { + 'DEFAULT/logging_context_format_string' : + value => $logging_context_format_string; + } + } + else { + heat_config { + 'DEFAULT/logging_context_format_string' : ensure => absent; + } + } + + if $logging_default_format_string { + heat_config { + 'DEFAULT/logging_default_format_string' : + value => $logging_default_format_string; + } + } + else { + heat_config { + 'DEFAULT/logging_default_format_string' : ensure => absent; + } + } + + if $logging_debug_format_suffix { + heat_config { + 'DEFAULT/logging_debug_format_suffix' : + value => $logging_debug_format_suffix; + } + } + else { + heat_config { + 'DEFAULT/logging_debug_format_suffix' : ensure => absent; + } + } + + if $logging_exception_prefix { + heat_config { + 'DEFAULT/logging_exception_prefix' : value => $logging_exception_prefix; + } + } + else { + heat_config { + 'DEFAULT/logging_exception_prefix' : ensure => absent; + } + } + + if $log_config_append { + heat_config { + 'DEFAULT/log_config_append' : value => $log_config_append; + } + } + else { + heat_config { + 'DEFAULT/log_config_append' : ensure => absent; + } + } + + if $default_log_levels { + heat_config { + 'DEFAULT/default_log_levels' : + value => join(sort(join_keys_to_values($default_log_levels, '=')), ','); + } + } + else { + heat_config { + 'DEFAULT/default_log_levels' : ensure => absent; + } + } + + if $publish_errors { + heat_config { + 'DEFAULT/publish_errors' : value => $publish_errors; + } + } + else { + heat_config { + 'DEFAULT/publish_errors' : ensure => absent; + } + } + + if $fatal_deprecations { + heat_config { + 'DEFAULT/fatal_deprecations' : value => $fatal_deprecations; + } + } + else { + heat_config { + 'DEFAULT/fatal_deprecations' : ensure => absent; + } + } + + if $instance_format { + heat_config { + 'DEFAULT/instance_format' : value => $instance_format; + } + } + else { + heat_config { + 'DEFAULT/instance_format' : ensure => absent; + } + } + + if $instance_uuid_format { + heat_config { + 'DEFAULT/instance_uuid_format' : value => $instance_uuid_format; + } + } + else { + heat_config { + 'DEFAULT/instance_uuid_format' : ensure => absent; + } + } + + if $log_date_format { + heat_config { + 'DEFAULT/log_date_format' : value => $log_date_format; + } + } + else { + heat_config { + 'DEFAULT/log_date_format' : ensure => absent; + } + } + + +} diff --git a/heat/manifests/params.pp b/heat/manifests/params.pp new file mode 100644 index 000000000..11504a390 --- /dev/null +++ b/heat/manifests/params.pp @@ -0,0 +1,52 @@ +# Parameters for puppet-heat +# +class heat::params { + + $dbsync_command = + 'heat-manage --config-file /etc/heat/heat.conf db_sync' + + case $::osfamily { + 'RedHat': { + # package names + $api_package_name = 'openstack-heat-api' + $api_cloudwatch_package_name = 'openstack-heat-api-cloudwatch' + $api_cfn_package_name = 'openstack-heat-api-cfn' + $engine_package_name = 'openstack-heat-engine' + $client_package_name = 'python-heatclient' + $common_package_name = 'openstack-heat-common' + # service names + $api_service_name = 'openstack-heat-api' + $api_cloudwatch_service_name = 'openstack-heat-api-cloudwatch' + $api_cfn_service_name = 'openstack-heat-api-cfn' + $engine_service_name = 'openstack-heat-engine' + } + 'Debian': { + # package names + $api_package_name = 'heat-api' + $api_cloudwatch_package_name = 'heat-api-cloudwatch' + $api_cfn_package_name = 'heat-api-cfn' + $engine_package_name = 'heat-engine' + $client_package_name = 'python-heatclient' + $common_package_name = 'heat-common' + # service names + $api_service_name = 'heat-api' + $api_cloudwatch_service_name = 'heat-api-cloudwatch' + $api_cfn_service_name = 'heat-api-cfn' + $engine_service_name = 'heat-engine' + # Operating system specific + case $::operatingsystem { + 'Ubuntu': { + $libvirt_group = 'libvirtd' + } + default: { + $libvirt_group = 'libvirt' + } + } + } + default: { + fail("Unsupported osfamily: ${::osfamily} operatingsystem: \ +${::operatingsystem}, module ${module_name} only support osfamily \ +RedHat and Debian") + } + } +} diff --git a/heat/spec/classes/heat_api_cfn_spec.rb b/heat/spec/classes/heat_api_cfn_spec.rb new file mode 100644 index 000000000..53c3f7326 --- /dev/null +++ b/heat/spec/classes/heat_api_cfn_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe 'heat::api_cfn' do + + let :params do + { :enabled => true, + :manage_service => true, + :bind_host => '127.0.0.1', + :bind_port => '1234', + :workers => '0' } + end + + shared_examples_for 'heat-api-cfn' do + + context 'config params' do + + it { should contain_class('heat') } + it { should contain_class('heat::params') } + + it { should contain_heat_config('heat_api_cfn/bind_host').with_value( params[:bind_host] ) } + it { should contain_heat_config('heat_api_cfn/bind_port').with_value( params[:bind_port] ) } + it { should contain_heat_config('heat_api_cfn/workers').with_value( params[:workers] ) } + + end + + context 'with SSL socket options set' do + let :params do + { + :use_ssl => true, + :cert_file => '/path/to/cert', + :key_file => '/path/to/key' + } + end + + it { should contain_heat_config('heat_api_cfn/cert_file').with_value('/path/to/cert') } + it { should contain_heat_config('heat_api_cfn/key_file').with_value('/path/to/key') } + end + + context 'with SSL socket options set with wrong parameters' do + let :params do + { + :use_ssl => true, + :key_file => '/path/to/key' + } + end + + it_raises 'a Puppet::Error', /The cert_file parameter is required when use_ssl is set to true/ + end + + context 'with SSL socket options set to false' do + let :params do + { + :use_ssl => false, + :cert_file => false, + :key_file => false + } + end + + it { should contain_heat_config('heat_api_cfn/cert_file').with_ensure('absent') } + it { should contain_heat_config('heat_api_cfn/key_file').with_ensure('absent') } + end + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures heat-api-cfn service' do + + should contain_service('heat-api-cfn').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:api_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true, + :subscribe => ['Exec[heat-dbsync]'] + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures heat-api-cfn service' do + + should contain_service('heat-api-cfn').with( + :ensure => nil, + :name => platform_params[:api_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true, + :subscribe => ['Exec[heat-dbsync]'] + ) + end + end + end + + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :api_service_name => 'heat-api-cfn' } + end + + it_configures 'heat-api-cfn' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :api_service_name => 'openstack-heat-api-cfn' } + end + + it_configures 'heat-api-cfn' + end + +end diff --git a/heat/spec/classes/heat_api_cloudwatch_spec.rb b/heat/spec/classes/heat_api_cloudwatch_spec.rb new file mode 100644 index 000000000..dd9f078fc --- /dev/null +++ b/heat/spec/classes/heat_api_cloudwatch_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +describe 'heat::api_cloudwatch' do + + let :params do + { :enabled => true, + :manage_service => true, + :bind_host => '127.0.0.1', + :bind_port => '1234', + :workers => '0' } + end + + shared_examples_for 'heat-api-cloudwatch' do + + context 'config params' do + + it { should contain_class('heat') } + it { should contain_class('heat::params') } + + it { should contain_heat_config('heat_api_cloudwatch/bind_host').with_value( params[:bind_host] ) } + it { should contain_heat_config('heat_api_cloudwatch/bind_port').with_value( params[:bind_port] ) } + it { should contain_heat_config('heat_api_cloudwatch/workers').with_value( params[:workers] ) } + + end + + context 'with SSL socket options set' do + let :params do + { + :use_ssl => true, + :cert_file => '/path/to/cert', + :key_file => '/path/to/key' + } + end + + it { should contain_heat_config('heat_api_cloudwatch/cert_file').with_value('/path/to/cert') } + it { should contain_heat_config('heat_api_cloudwatch/key_file').with_value('/path/to/key') } + end + + context 'with SSL socket options set with wrong parameters' do + let :params do + { + :use_ssl => true, + :key_file => '/path/to/key' + } + end + + it_raises 'a Puppet::Error', /The cert_file parameter is required when use_ssl is set to true/ + end + + context 'with SSL socket options set to false' do + let :params do + { + :use_ssl => false, + :cert_file => false, + :key_file => false + } + end + + it { should contain_heat_config('heat_api_cloudwatch/cert_file').with_ensure('absent') } + it { should contain_heat_config('heat_api_cloudwatch/key_file').with_ensure('absent') } + end + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures heat-api-cloudwatch service' do + + should contain_service('heat-api-cloudwatch').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:api_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true, + :subscribe => ['Exec[heat-dbsync]'] + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures heat-api-cloudwatch service' do + + should contain_service('heat-api-cloudwatch').with( + :ensure => nil, + :name => platform_params[:api_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true, + :subscribe => ['Exec[heat-dbsync]'] + ) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :api_service_name => 'heat-api-cloudwatch' } + end + + it_configures 'heat-api-cloudwatch' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :api_service_name => 'openstack-heat-api-cloudwatch' } + end + + it_configures 'heat-api-cloudwatch' + end + +end diff --git a/heat/spec/classes/heat_api_spec.rb b/heat/spec/classes/heat_api_spec.rb new file mode 100644 index 000000000..20d4caa16 --- /dev/null +++ b/heat/spec/classes/heat_api_spec.rb @@ -0,0 +1,131 @@ +require 'spec_helper' + +describe 'heat::api' do + + let :params do + { :enabled => true, + :manage_service => true, + :bind_host => '127.0.0.1', + :bind_port => '1234', + :workers => '0' } + end + + shared_examples_for 'heat-api' do + + context 'config params' do + + it { should contain_class('heat') } + it { should contain_class('heat::params') } + + it { should contain_heat_config('heat_api/bind_host').with_value( params[:bind_host] ) } + it { should contain_heat_config('heat_api/bind_port').with_value( params[:bind_port] ) } + it { should contain_heat_config('heat_api/workers').with_value( params[:workers] ) } + + end + + context 'with SSL socket options set' do + let :params do + { + :use_ssl => true, + :cert_file => '/path/to/cert', + :key_file => '/path/to/key' + } + end + + it { should contain_heat_config('heat_api/cert_file').with_value('/path/to/cert') } + it { should contain_heat_config('heat_api/key_file').with_value('/path/to/key') } + end + + context 'with SSL socket options set with wrong parameters' do + let :params do + { + :use_ssl => true, + :key_file => '/path/to/key' + } + end + + it_raises 'a Puppet::Error', /The cert_file parameter is required when use_ssl is set to true/ + end + + context 'with SSL socket options set to false' do + let :params do + { + :use_ssl => false, + :cert_file => false, + :key_file => false + } + end + + it { should contain_heat_config('heat_api/cert_file').with_ensure('absent') } + it { should contain_heat_config('heat_api/key_file').with_ensure('absent') } + end + + + [{:enabled => true}, {:enabled => false}].each do |param_hash| + context "when service should be #{param_hash[:enabled] ? 'enabled' : 'disabled'}" do + before do + params.merge!(param_hash) + end + + it 'configures heat-api service' do + + should contain_service('heat-api').with( + :ensure => (params[:manage_service] && params[:enabled]) ? 'running' : 'stopped', + :name => platform_params[:api_service_name], + :enable => params[:enabled], + :hasstatus => true, + :hasrestart => true, + :require => ['Package[heat-common]', 'Package[heat-api]'], + :subscribe => ['Exec[heat-dbsync]'] + ) + end + end + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it 'configures heat-api service' do + + should contain_service('heat-api').with( + :ensure => nil, + :name => platform_params[:api_service_name], + :enable => false, + :hasstatus => true, + :hasrestart => true, + :require => ['Package[heat-common]', 'Package[heat-api]'], + :subscribe => ['Exec[heat-dbsync]'] + ) + end + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :api_service_name => 'heat-api' } + end + + it_configures 'heat-api' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :api_service_name => 'openstack-heat-api' } + end + + it_configures 'heat-api' + end + +end diff --git a/heat/spec/classes/heat_db_mysql_spec.rb b/heat/spec/classes/heat_db_mysql_spec.rb new file mode 100644 index 000000000..1f2803a3b --- /dev/null +++ b/heat/spec/classes/heat_db_mysql_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe 'heat::db::mysql' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :params do + { :password => 's3cr3t', + :dbname => 'heat', + :user => 'heat', + :host => 'localhost', + :charset => 'utf8', + } + end + + shared_examples_for 'heat mysql database' do + + context 'when omiting the required parameter password' do + before { params.delete(:password) } + it { expect { should raise_error(Puppet::Error) } } + end + + it 'creates a mysql database' do + should contain_openstacklib__db__mysql( params[:dbname] ).with( + :user => params[:user], + :password_hash => '*58C036CDA51D8E8BBBBF2F9EA5ABF111ADA444F0', + :host => params[:host], + :charset => params[:charset], + :require => 'Class[Mysql::Config]' + ) + end + end + + describe "overriding allowed_hosts param to array" do + let :params do + { + :password => 'heatpass', + :allowed_hosts => ['localhost','%'] + } + end + + end + + describe "overriding allowed_hosts param to string" do + let :params do + { + :password => 'heatpass2', + :allowed_hosts => '192.168.1.1' + } + end + + end + + describe "overriding allowed_hosts param equals to host param " do + let :params do + { + :password => 'heatpass2', + :allowed_hosts => 'localhost' + } + end + + end +end diff --git a/heat/spec/classes/heat_engine_spec.rb b/heat/spec/classes/heat_engine_spec.rb new file mode 100644 index 000000000..d4639ba84 --- /dev/null +++ b/heat/spec/classes/heat_engine_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe 'heat::engine' do + + let :default_params do + { :enabled => true, + :manage_service => true, + :heat_stack_user_role => 'heat_stack_user', + :heat_metadata_server_url => 'http://127.0.0.1:8000', + :heat_waitcondition_server_url => 'http://127.0.0.1:8000/v1/waitcondition', + :heat_watch_server_url => 'http://128.0.0.1:8003', + :engine_life_check_timeout => '2', + } + end + + shared_examples_for 'heat-engine' do + [ + {}, + { :auth_encryption_key => '1234567890AZERTYUIOPMLKJHGFDSQ' }, + { :auth_encryption_key => 'foodummybar', + :enabled => false, + :heat_stack_user_role => 'heat_stack_user', + :heat_metadata_server_url => 'http://127.0.0.1:8000', + :heat_waitcondition_server_url => 'http://127.0.0.1:8000/v1/waitcondition', + :heat_watch_server_url => 'http://128.0.0.1:8003', + :engine_life_check_timeout => '2', + } + ].each do |new_params| + describe 'when #{param_set == {} ? "using default" : "specifying"} parameters' + + let :params do + new_params + end + + let :expected_params do + default_params.merge(params) + end + + it { should contain_package('heat-engine').with_name(os_params[:package_name]) } + + it { should contain_service('heat-engine').with( + :ensure => (expected_params[:manage_service] && expected_params[:enabled]) ? 'running' : 'stopped', + :name => os_params[:service_name], + :enable => expected_params[:enabled], + :hasstatus => 'true', + :hasrestart => 'true', + :require => [ 'File[/etc/heat/heat.conf]', + 'Package[heat-common]', + 'Package[heat-engine]'], + :subscribe => 'Exec[heat-dbsync]' + ) } + + it { should contain_heat_config('DEFAULT/auth_encryption_key').with_value( expected_params[:auth_encryption_key] ) } + it { should contain_heat_config('DEFAULT/heat_stack_user_role').with_value( expected_params[:heat_stack_user_role] ) } + it { should contain_heat_config('DEFAULT/heat_metadata_server_url').with_value( expected_params[:heat_metadata_server_url] ) } + it { should contain_heat_config('DEFAULT/heat_waitcondition_server_url').with_value( expected_params[:heat_waitcondition_server_url] ) } + it { should contain_heat_config('DEFAULT/heat_watch_server_url').with_value( expected_params[:heat_watch_server_url] ) } + it { should contain_heat_config('DEFAULT/engine_life_check_timeout').with_value( expected_params[:engine_life_check_timeout] ) } + end + + context 'with disabled service managing' do + before do + params.merge!({ + :manage_service => false, + :enabled => false }) + end + + it { should contain_service('heat-engine').with( + :ensure => nil, + :name => os_params[:service_name], + :enable => false, + :hasstatus => 'true', + :hasrestart => 'true', + :require => [ 'File[/etc/heat/heat.conf]', + 'Package[heat-common]', + 'Package[heat-engine]'], + :subscribe => 'Exec[heat-dbsync]' + ) } + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :os_params do + { :package_name => 'heat-engine', + :service_name => 'heat-engine' + } + end + + it_configures 'heat-engine' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :os_params do + { :package_name => 'openstack-heat-engine', + :service_name => 'openstack-heat-engine' + } + end + + it_configures 'heat-engine' + end +end diff --git a/heat/spec/classes/heat_init_spec.rb b/heat/spec/classes/heat_init_spec.rb new file mode 100644 index 000000000..82daadf4c --- /dev/null +++ b/heat/spec/classes/heat_init_spec.rb @@ -0,0 +1,415 @@ +require 'spec_helper' + +describe 'heat' do + + let :params do + { + :package_ensure => 'present', + :verbose => 'False', + :debug => 'False', + :log_dir => '/var/log/heat', + :rabbit_host => '127.0.0.1', + :rabbit_port => 5672, + :rabbit_userid => 'guest', + :rabbit_password => '', + :rabbit_virtual_host => '/', + :sql_connection => 'mysql://user@host/database', + :database_idle_timeout => 3600, + :auth_uri => 'http://127.0.0.1:5000/v2.0', + :keystone_ec2_uri => 'http://127.0.0.1:5000/v2.0/ec2tokens', + } + end + + let :qpid_params do + { + :rpc_backend => "heat.openstack.common.rpc.impl_qpid", + :qpid_hostname => 'localhost', + :qpid_port => 5672, + :qpid_username => 'guest', + :qpid_password => 'guest', + } + end + + shared_examples_for 'heat' do + + context 'with rabbit_host parameter' do + it_configures 'a heat base installation' + it_configures 'rabbit without HA support (with backward compatibility)' + end + + context 'with rabbit_hosts parameter' do + context 'with one server' do + before { params.merge!( :rabbit_hosts => ['127.0.0.1:5672'] ) } + it_configures 'a heat base installation' + it_configures 'rabbit without HA support (without backward compatibility)' + end + + context 'with multiple servers' do + before { params.merge!( + :rabbit_hosts => ['rabbit1:5672', 'rabbit2:5672'], + :amqp_durable_queues => true) } + it_configures 'a heat base installation' + it_configures 'rabbit with HA support' + end + end + + context 'with qpid instance' do + before {params.merge!(qpid_params) } + + it_configures 'a heat base installation' + it_configures 'qpid as rpc backend' + end + + it_configures 'with syslog disabled' + it_configures 'with syslog enabled' + it_configures 'with syslog enabled and custom settings' + it_configures 'with SSL enabled with kombu' + it_configures 'with SSL enabled without kombu' + it_configures 'with SSL disabled' + it_configures 'with SSL wrongly configured' + end + + shared_examples_for 'a heat base installation' do + + it { should contain_class('heat::params') } + + it 'configures heat group' do + should contain_group('heat').with( + :name => 'heat', + :require => 'Package[heat-common]' + ) + end + + it 'configures heat user' do + should contain_user('heat').with( + :name => 'heat', + :gid => 'heat', + :groups => ['heat'], + :system => true, + :require => 'Package[heat-common]' + ) + end + + it 'configures heat configuration folder' do + should contain_file('/etc/heat/').with( + :ensure => 'directory', + :owner => 'heat', + :group => 'heat', + :mode => '0750', + :require => 'Package[heat-common]' + ) + end + + it 'configures heat configuration file' do + should contain_file('/etc/heat/heat.conf').with( + :owner => 'heat', + :group => 'heat', + :mode => '0640', + :require => 'Package[heat-common]' + ) + end + + it 'installs heat common package' do + should contain_package('heat-common').with( + :ensure => 'present', + :name => platform_params[:common_package_name] + ) + end + + + it 'configures debug and verbose' do + should contain_heat_config('DEFAULT/debug').with_value( params[:debug] ) + should contain_heat_config('DEFAULT/verbose').with_value( params[:verbose] ) + end + + it 'configures auth_uri' do + should contain_heat_config('keystone_authtoken/auth_uri').with_value( params[:auth_uri] ) + end + + it 'configures logging directory by default' do + should contain_heat_config('DEFAULT/log_dir').with_value( params[:log_dir] ) + end + + context 'with logging directory disabled' do + before { params.merge!( :log_dir => false) } + + it { should contain_heat_config('DEFAULT/log_dir').with_ensure('absent') } + end + + it 'configures sql_connection' do + should contain_heat_config('database/connection').with_value( params[:sql_connection] ) + end + + it 'configures database_idle_timeout' do + should contain_heat_config('database/idle_timeout').with_value( params[:database_idle_timeout] ) + end + + context("failing if sql_connection is invalid") do + before { params[:sql_connection] = 'foo://foo:bar@baz/moo' } + it { expect { should raise_error(Puppet::Error) } } + end + + it 'configures keystone_ec2_uri' do + should contain_heat_config('ec2authtoken/auth_uri').with_value( params[:keystone_ec2_uri] ) + end + + end + + shared_examples_for 'rabbit without HA support (with backward compatibility)' do + it 'configures rabbit' do + should contain_heat_config('DEFAULT/rabbit_userid').with_value( params[:rabbit_userid] ) + should contain_heat_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ) + should contain_heat_config('DEFAULT/rabbit_password').with_secret( true ) + should contain_heat_config('DEFAULT/rabbit_virtual_host').with_value( params[:rabbit_virtual_host] ) + should contain_heat_config('DEFAULT/rabbit_use_ssl').with_value(false) + should contain_heat_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_version').with_ensure('absent') + end + it { should contain_heat_config('DEFAULT/rabbit_host').with_value( params[:rabbit_host] ) } + it { should contain_heat_config('DEFAULT/rabbit_port').with_value( params[:rabbit_port] ) } + it { should contain_heat_config('DEFAULT/rabbit_hosts').with_value( "#{params[:rabbit_host]}:#{params[:rabbit_port]}" ) } + it { should contain_heat_config('DEFAULT/rabbit_ha_queues').with_value('false') } + it { should contain_heat_config('DEFAULT/amqp_durable_queues').with_value(false) } + end + + shared_examples_for 'rabbit without HA support (without backward compatibility)' do + it 'configures rabbit' do + should contain_heat_config('DEFAULT/rabbit_userid').with_value( params[:rabbit_userid] ) + should contain_heat_config('DEFAULT/rabbit_password').with_secret( true ) + should contain_heat_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ) + should contain_heat_config('DEFAULT/rabbit_virtual_host').with_value( params[:rabbit_virtual_host] ) + should contain_heat_config('DEFAULT/rabbit_use_ssl').with_value(false) + should contain_heat_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_version').with_ensure('absent') + end + it { should contain_heat_config('DEFAULT/rabbit_host').with_ensure('absent') } + it { should contain_heat_config('DEFAULT/rabbit_port').with_ensure('absent') } + it { should contain_heat_config('DEFAULT/rabbit_hosts').with_value( params[:rabbit_hosts].join(',') ) } + it { should contain_heat_config('DEFAULT/rabbit_ha_queues').with_value('false') } + it { should contain_heat_config('DEFAULT/amqp_durable_queues').with_value(false) } + end + + shared_examples_for 'rabbit with HA support' do + it 'configures rabbit' do + should contain_heat_config('DEFAULT/rabbit_userid').with_value( params[:rabbit_userid] ) + should contain_heat_config('DEFAULT/rabbit_password').with_value( params[:rabbit_password] ) + should contain_heat_config('DEFAULT/rabbit_password').with_secret( true ) + should contain_heat_config('DEFAULT/rabbit_virtual_host').with_value( params[:rabbit_virtual_host] ) + should contain_heat_config('DEFAULT/rabbit_use_ssl').with_value(false) + should contain_heat_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_version').with_ensure('absent') + end + it { should contain_heat_config('DEFAULT/rabbit_host').with_ensure('absent') } + it { should contain_heat_config('DEFAULT/rabbit_port').with_ensure('absent') } + it { should contain_heat_config('DEFAULT/rabbit_hosts').with_value( params[:rabbit_hosts].join(',') ) } + it { should contain_heat_config('DEFAULT/rabbit_ha_queues').with_value('true') } + it { should contain_heat_config('DEFAULT/amqp_durable_queues').with_value(true) } + end + + + shared_examples_for 'qpid as rpc backend' do + context("with default parameters") do + it { should contain_heat_config('DEFAULT/qpid_reconnect').with_value(true) } + it { should contain_heat_config('DEFAULT/qpid_reconnect_timeout').with_value('0') } + it { should contain_heat_config('DEFAULT/qpid_reconnect_limit').with_value('0') } + it { should contain_heat_config('DEFAULT/qpid_reconnect_interval_min').with_value('0') } + it { should contain_heat_config('DEFAULT/qpid_reconnect_interval_max').with_value('0') } + it { should contain_heat_config('DEFAULT/qpid_reconnect_interval').with_value('0') } + it { should contain_heat_config('DEFAULT/qpid_heartbeat').with_value('60') } + it { should contain_heat_config('DEFAULT/qpid_protocol').with_value('tcp') } + it { should contain_heat_config('DEFAULT/qpid_tcp_nodelay').with_value(true) } + it { should contain_heat_config('DEFAULT/amqp_durable_queues').with_value(false) } + end + + context("with mandatory parameters set") do + it { should contain_heat_config('DEFAULT/rpc_backend').with_value('heat.openstack.common.rpc.impl_qpid') } + it { should contain_heat_config('DEFAULT/qpid_hostname').with_value( params[:qpid_hostname] ) } + it { should contain_heat_config('DEFAULT/qpid_port').with_value( params[:qpid_port] ) } + it { should contain_heat_config('DEFAULT/qpid_username').with_value( params[:qpid_username]) } + it { should contain_heat_config('DEFAULT/qpid_password').with_value(params[:qpid_password]) } + it { should contain_heat_config('DEFAULT/qpid_password').with_secret( true ) } + end + + context("failing if the rpc_backend is not present") do + before { params.delete( :rpc_backend) } + it { expect { should raise_error(Puppet::Error) } } + end + end + + shared_examples_for 'with SSL enabled with kombu' do + before do + params.merge!( + :rabbit_use_ssl => true, + :kombu_ssl_ca_certs => '/path/to/ssl/ca/certs', + :kombu_ssl_certfile => '/path/to/ssl/cert/file', + :kombu_ssl_keyfile => '/path/to/ssl/keyfile', + :kombu_ssl_version => 'SSLv3' + ) + end + + it do + should contain_heat_config('DEFAULT/rabbit_use_ssl').with_value('true') + should contain_heat_config('DEFAULT/kombu_ssl_ca_certs').with_value('/path/to/ssl/ca/certs') + should contain_heat_config('DEFAULT/kombu_ssl_certfile').with_value('/path/to/ssl/cert/file') + should contain_heat_config('DEFAULT/kombu_ssl_keyfile').with_value('/path/to/ssl/keyfile') + should contain_heat_config('DEFAULT/kombu_ssl_version').with_value('SSLv3') + end + end + + shared_examples_for 'with SSL enabled without kombu' do + before do + params.merge!( + :rabbit_use_ssl => true + ) + end + + it do + should contain_heat_config('DEFAULT/rabbit_use_ssl').with_value('true') + should contain_heat_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_version').with_value('SSLv3') + end + end + + shared_examples_for 'with SSL disabled' do + before do + params.merge!( + :rabbit_use_ssl => false, + :kombu_ssl_version => 'SSLv3' + ) + end + + it do + should contain_heat_config('DEFAULT/rabbit_use_ssl').with_value('false') + should contain_heat_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') + should contain_heat_config('DEFAULT/kombu_ssl_version').with_ensure('absent') + end + end + + shared_examples_for 'with SSL wrongly configured' do + before do + params.merge!( + :rabbit_use_ssl => false + ) + end + + context 'without required parameters' do + + context 'with rabbit_use_ssl => false and kombu_ssl_ca_certs parameter' do + before { params.merge!(:kombu_ssl_ca_certs => '/path/to/ssl/ca/certs') } + it_raises 'a Puppet::Error', /The kombu_ssl_ca_certs parameter requires rabbit_use_ssl to be set to true/ + end + + context 'with rabbit_use_ssl => false and kombu_ssl_certfile parameter' do + before { params.merge!(:kombu_ssl_certfile => '/path/to/ssl/cert/file') } + it_raises 'a Puppet::Error', /The kombu_ssl_certfile parameter requires rabbit_use_ssl to be set to true/ + end + + context 'with rabbit_use_ssl => false and kombu_ssl_keyfile parameter' do + before { params.merge!(:kombu_ssl_keyfile => '/path/to/ssl/keyfile') } + it_raises 'a Puppet::Error', /The kombu_ssl_keyfile parameter requires rabbit_use_ssl to be set to true/ + end + end + + end + + shared_examples_for 'with syslog disabled' do + it { should contain_heat_config('DEFAULT/use_syslog').with_value(false) } + end + + shared_examples_for 'with syslog enabled' do + before do + params.merge!( + :use_syslog => 'true' + ) + end + + it do + should contain_heat_config('DEFAULT/use_syslog').with_value(true) + should contain_heat_config('DEFAULT/syslog_log_facility').with_value('LOG_USER') + end + end + + shared_examples_for 'with syslog enabled and custom settings' do + before do + params.merge!( + :use_syslog => 'true', + :log_facility => 'LOG_LOCAL0' + ) + end + + it do + should contain_heat_config('DEFAULT/use_syslog').with_value(true) + should contain_heat_config('DEFAULT/syslog_log_facility').with_value('LOG_LOCAL0') + end + end + + shared_examples_for 'with database_idle_timeout modified' do + before do + params.merge!( + :database_idle_timeout => 69 + ) + end + + it do + should contain_heat_config('database/idle_timeout').with_value(69) + end + end + + shared_examples_for 'with ec2authtoken auth uri set' do + before do + params.merge!( + :keystone_ec2_uri => 'http://1.2.3.4:35357/v2.0/ec2tokens' + ) + end + + it do + should contain_heat_config('ec2authtoken/auth_uri').with_value('http://1.2.3.4:35357/v2.0/ec2tokens') + end + end + + shared_examples_for 'with auth uri set' do + before do + params.merge!( + :auth_uri => 'http://1.2.3.4:35357/v2.0' + ) + end + + it do + should contain_heat_config('keystone_authtoken/auth_uri').with_value('http://1.2.3.4:35357/v2.0') + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + let :platform_params do + { :common_package_name => 'heat-common' } + end + + it_configures 'heat' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + let :platform_params do + { :common_package_name => 'openstack-heat-common' } + end + + it_configures 'heat' + end +end diff --git a/heat/spec/classes/heat_keystone_auth_cfn_spec.rb b/heat/spec/classes/heat_keystone_auth_cfn_spec.rb new file mode 100644 index 000000000..d81b3dcea --- /dev/null +++ b/heat/spec/classes/heat_keystone_auth_cfn_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe 'heat::keystone::auth_cfn' do + + let :params do + { + :password => 'heat-passw0rd', + :email => 'heat-cfn@localhost', + :auth_name => 'heat-cfn', + :configure_endpoint => true, + :service_type => 'cloudformation', + :public_address => '127.0.0.1', + :admin_address => '127.0.0.1', + :internal_address => '127.0.0.1', + :port => '8000', + :version => 'v1', + :region => 'RegionOne', + :tenant => 'services', + :public_protocol => 'http', + :admin_protocol => 'http', + :internal_protocol => 'http', + } + end + + shared_examples_for 'heat keystone auth' do + + context 'without the required password parameter' do + before { params.delete(:password) } + it { expect { should raise_error(Puppet::Error) } } + end + + it 'configures heat user' do + should contain_keystone_user( params[:auth_name] ).with( + :ensure => 'present', + :password => params[:password], + :email => params[:email], + :tenant => params[:tenant] + ) + end + + it 'configures heat user roles' do + should contain_keystone_user_role("#{params[:auth_name]}@#{params[:tenant]}").with( + :ensure => 'present', + :roles => ['admin'] + ) + end + + it 'configures heat service' do + should contain_keystone_service( params[:auth_name] ).with( + :ensure => 'present', + :type => params[:service_type], + :description => 'Openstack Cloudformation Service' + ) + end + + it 'configure heat endpoints' do + should contain_keystone_endpoint("#{params[:region]}/#{params[:auth_name]}").with( + :ensure => 'present', + :public_url => "#{params[:public_protocol]}://#{params[:public_address]}:#{params[:port]}/#{params[:version]}/", + :admin_url => "#{params[:admin_protocol]}://#{params[:admin_address]}:#{params[:port]}/#{params[:version]}/", + :internal_url => "#{params[:internal_protocol]}://#{params[:internal_address]}:#{params[:port]}/#{params[:version]}/" + ) + end + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'heat keystone auth' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'heat keystone auth' + end + + context 'when overriding service name' do + before do + params.merge!({ + :service_name => 'heat-cfn_service' + }) + end + it 'configures correct user name' do + should contain_keystone_user('heat-cfn') + end + it 'configures correct user role' do + should contain_keystone_user_role('heat-cfn@services') + end + it 'configures correct service name' do + should contain_keystone_service('heat-cfn_service') + end + it 'configures correct endpoint name' do + should contain_keystone_endpoint('RegionOne/heat-cfn_service') + end + end + + context 'when disabling user configuration' do + before do + params.merge!( :configure_user => false ) + end + + it { should_not contain_keystone_user('heat_cfn') } + it { should contain_keystone_user_role('heat-cfn@services') } + + it { should contain_keystone_service('heat-cfn').with( + :ensure => 'present', + :type => 'cloudformation', + :description => 'Openstack Cloudformation Service' + )} + end + + context 'when disabling user and role configuration' do + before do + params.merge!( + :configure_user => false, + :configure_user_role => false + ) + end + + it { should_not contain_keystone_user('heat_cfn') } + it { should_not contain_keystone_user_role('heat-cfn@services') } + + it { should contain_keystone_service('heat-cfn').with( + :ensure => 'present', + :type => 'cloudformation', + :description => 'Openstack Cloudformation Service' + )} + end + +end diff --git a/heat/spec/classes/heat_keystone_auth_spec.rb b/heat/spec/classes/heat_keystone_auth_spec.rb new file mode 100644 index 000000000..dd02f88c7 --- /dev/null +++ b/heat/spec/classes/heat_keystone_auth_spec.rb @@ -0,0 +1,143 @@ +require 'spec_helper' + +describe 'heat::keystone::auth' do + + let :params do + { + :password => 'heat-passw0rd', + :email => 'heat@localhost', + :auth_name => 'heat', + :configure_endpoint => true, + :service_type => 'orchestration', + :public_address => '127.0.0.1', + :admin_address => '127.0.0.1', + :internal_address => '127.0.0.1', + :port => '8004', + :version => 'v1', + :region => 'RegionOne', + :tenant => 'services', + :public_protocol => 'http', + :admin_protocol => 'http', + :internal_protocol => 'http', + } + end + + shared_examples_for 'heat keystone auth' do + + context 'without the required password parameter' do + before { params.delete(:password) } + it { expect { should raise_error(Puppet::Error) } } + end + + it 'configures heat user' do + should contain_keystone_user( params[:auth_name] ).with( + :ensure => 'present', + :password => params[:password], + :email => params[:email], + :tenant => params[:tenant] + ) + end + + it 'configures heat user roles' do + should contain_keystone_user_role("#{params[:auth_name]}@#{params[:tenant]}").with( + :ensure => 'present', + :roles => ['admin'] + ) + end + + it 'configures heat stack_user role' do + should contain_keystone_role("heat_stack_user").with( + :ensure => 'present' + ) + end + + + it 'configures heat service' do + should contain_keystone_service( params[:auth_name] ).with( + :ensure => 'present', + :type => params[:service_type], + :description => 'Openstack Orchestration Service' + ) + end + + it 'configure heat endpoints' do + should contain_keystone_endpoint("#{params[:region]}/#{params[:auth_name]}").with( + :ensure => 'present', + :public_url => "#{params[:public_protocol]}://#{params[:public_address]}:#{params[:port]}/#{params[:version]}/%(tenant_id)s", + :admin_url => "#{params[:admin_protocol]}://#{params[:admin_address]}:#{params[:port]}/#{params[:version]}/%(tenant_id)s", + :internal_url => "#{params[:internal_protocol]}://#{params[:internal_address]}:#{params[:port]}/#{params[:version]}/%(tenant_id)s" + ) + end + end + + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'heat keystone auth' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'heat keystone auth' + end + + context 'when overriding service name' do + before do + params.merge!({ + :service_name => 'heat_service' + }) + end + it 'configures correct user name' do + should contain_keystone_user('heat') + end + it 'configures correct user role' do + should contain_keystone_user_role('heat@services') + end + it 'configures correct service name' do + should contain_keystone_service('heat_service') + end + it 'configures correct endpoint name' do + should contain_keystone_endpoint('RegionOne/heat_service') + end + end + + context 'when disabling user configuration' do + before do + params.merge!( :configure_user => false ) + end + + it { should_not contain_keystone_user('heat') } + it { should contain_keystone_user_role('heat@services') } + + it { should contain_keystone_service('heat').with( + :ensure => 'present', + :type => 'orchestration', + :description => 'Openstack Orchestration Service' + )} + end + + context 'when disabling user and role configuration' do + before do + params.merge!( + :configure_user => false, + :configure_user_role => false + ) + end + + it { should_not contain_keystone_user('heat') } + it { should_not contain_keystone_user_role('heat@services') } + + it { should contain_keystone_service('heat').with( + :ensure => 'present', + :type => 'orchestration', + :description => 'Openstack Orchestration Service' + )} + end + +end diff --git a/heat/spec/classes/heat_logging_spec.rb b/heat/spec/classes/heat_logging_spec.rb new file mode 100644 index 000000000..c025d5eb7 --- /dev/null +++ b/heat/spec/classes/heat_logging_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe 'heat::logging' do + + let :params do + { + } + end + + let :log_params do + { + :logging_context_format_string => '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s', + :logging_default_format_string => '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s', + :logging_debug_format_suffix => '%(funcName)s %(pathname)s:%(lineno)d', + :logging_exception_prefix => '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s', + :log_config_append => '/etc/heat/logging.conf', + :publish_errors => true, + :default_log_levels => { + 'amqp' => 'WARN', 'amqplib' => 'WARN', 'boto' => 'WARN', + 'qpid' => 'WARN', 'sqlalchemy' => 'WARN', 'suds' => 'INFO', + 'iso8601' => 'WARN', + 'requests.packages.urllib3.connectionpool' => 'WARN' }, + :fatal_deprecations => true, + :instance_format => '[instance: %(uuid)s] ', + :instance_uuid_format => '[instance: %(uuid)s] ', + :log_date_format => '%Y-%m-%d %H:%M:%S', + } + end + + shared_examples_for 'heat-logging' do + + context 'with extended logging options' do + before { params.merge!( log_params ) } + it_configures 'logging params set' + end + + context 'without extended logging options' do + it_configures 'logging params unset' + end + + end + + shared_examples_for 'logging params set' do + it 'enables logging params' do + should contain_heat_config('DEFAULT/logging_context_format_string').with_value( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s') + + should contain_heat_config('DEFAULT/logging_default_format_string').with_value( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s') + + should contain_heat_config('DEFAULT/logging_debug_format_suffix').with_value( + '%(funcName)s %(pathname)s:%(lineno)d') + + should contain_heat_config('DEFAULT/logging_exception_prefix').with_value( + '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s') + + should contain_heat_config('DEFAULT/log_config_append').with_value( + '/etc/heat/logging.conf') + should contain_heat_config('DEFAULT/publish_errors').with_value( + true) + + should contain_heat_config('DEFAULT/default_log_levels').with_value( + 'amqp=WARN,amqplib=WARN,boto=WARN,iso8601=WARN,qpid=WARN,requests.packages.urllib3.connectionpool=WARN,sqlalchemy=WARN,suds=INFO') + + should contain_heat_config('DEFAULT/fatal_deprecations').with_value( + true) + + should contain_heat_config('DEFAULT/instance_format').with_value( + '[instance: %(uuid)s] ') + + should contain_heat_config('DEFAULT/instance_uuid_format').with_value( + '[instance: %(uuid)s] ') + + should contain_heat_config('DEFAULT/log_date_format').with_value( + '%Y-%m-%d %H:%M:%S') + end + end + + + shared_examples_for 'logging params unset' do + [ :logging_context_format_string, :logging_default_format_string, + :logging_debug_format_suffix, :logging_exception_prefix, + :log_config_append, :publish_errors, + :default_log_levels, :fatal_deprecations, + :instance_format, :instance_uuid_format, + :log_date_format, ].each { |param| + it { should contain_heat_config("DEFAULT/#{param}").with_ensure('absent') } + } + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'heat-logging' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'heat-logging' + end + +end diff --git a/heat/spec/shared_examples.rb b/heat/spec/shared_examples.rb new file mode 100644 index 000000000..d92156a36 --- /dev/null +++ b/heat/spec/shared_examples.rb @@ -0,0 +1,5 @@ +shared_examples_for "a Puppet::Error" do |description| + it "with message matching #{description.inspect}" do + expect { should have_class_count(1) }.to raise_error(Puppet::Error, description) + end +end diff --git a/heat/spec/spec_helper.rb b/heat/spec/spec_helper.rb new file mode 100644 index 000000000..53d4dd02d --- /dev/null +++ b/heat/spec/spec_helper.rb @@ -0,0 +1,7 @@ +require 'puppetlabs_spec_helper/module_spec_helper' +require 'shared_examples' + +RSpec.configure do |c| + c.alias_it_should_behave_like_to :it_configures, 'configures' + c.alias_it_should_behave_like_to :it_raises, 'raises' +end diff --git a/horizon b/horizon deleted file mode 160000 index 16b482ea2..000000000 --- a/horizon +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 16b482ea21a70d8dd06ab4c98ac5a218399b0213 diff --git a/horizon/.fixtures.yml b/horizon/.fixtures.yml new file mode 100644 index 000000000..daaf73dda --- /dev/null +++ b/horizon/.fixtures.yml @@ -0,0 +1,8 @@ +fixtures: + repositories: + "apache": "git://github.com/puppetlabs/puppetlabs-apache.git" + "concat": "git://github.com/ripienaar/puppet-concat.git" + "firewall": "git://github.com/puppetlabs/puppetlabs-firewall.git" + "stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib.git" + symlinks: + "horizon": "#{source_dir}" diff --git a/horizon/.gitignore b/horizon/.gitignore new file mode 100644 index 000000000..1fc755c8f --- /dev/null +++ b/horizon/.gitignore @@ -0,0 +1,5 @@ +Gemfile.lock +spec/fixtures/modules/* +spec/fixtures/manifests/site.pp +*.swp +pkg diff --git a/horizon/.gitreview b/horizon/.gitreview new file mode 100644 index 000000000..14c44d48d --- /dev/null +++ b/horizon/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=stackforge/puppet-horizon.git diff --git a/horizon/Gemfile b/horizon/Gemfile new file mode 100644 index 000000000..d965fa900 --- /dev/null +++ b/horizon/Gemfile @@ -0,0 +1,18 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'puppetlabs_spec_helper', :require => false + gem 'puppet-lint', '~> 0.3.2' + gem 'rake', '10.1.1' + gem 'rspec', '< 2.99' + gem 'json' + gem 'webmock' +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/horizon/LICENSE b/horizon/LICENSE new file mode 100644 index 000000000..8961ce8a6 --- /dev/null +++ b/horizon/LICENSE @@ -0,0 +1,15 @@ +Copyright (C) 2012 Puppet Labs Inc + +Puppet Labs can be contacted at: info@puppetlabs.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/horizon/Modulefile b/horizon/Modulefile new file mode 100644 index 000000000..218752463 --- /dev/null +++ b/horizon/Modulefile @@ -0,0 +1,12 @@ +name 'puppetlabs-horizon' +version '4.0.0' +author 'Puppet Labs and StackForge Contributors' +license 'Apache License 2.0' +summary 'Puppet module for OpenStack Horizon' +description 'Installs and configures OpenStack Horizon (Dashboard).' +project_page 'https://launchpad.net/puppet-horizon' +source 'https://github.com/stackforge/puppet-horizon' + +dependency 'puppetlabs/apache', '>= 1.0.0 <2.0.0' +dependency 'puppetlabs/stdlib', '>= 3.2.0' +dependency 'saz/memcached', '>= 2.0.2 <3.0.0' diff --git a/horizon/README.md b/horizon/README.md new file mode 100644 index 000000000..56bd56e7e --- /dev/null +++ b/horizon/README.md @@ -0,0 +1,138 @@ +horizon +======= + +4.0.0 - 2014.1.0 - Icehouse + +#### Table of Contents + +1. [Overview - What is the horizon module?](#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with horizon](#setup) +4. [Implementation - An under-the-hood peek at what the module is doing](#implementation) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) +7. [Contributors - Those with commits](#contributors) +8. [Release Notes - Notes on the most recent updates to the module](#release-notes) + +Overview +-------- + +The horizon module is a part of [Stackforge](https://github.com/stackforge), an effort by the Openstack infrastructure team to provide continuous integration testing and code review for Openstack and Openstack community projects not part of the core software. The module its self is used to flexibly configure and manage the dashboard service for Openstack. + +Module Description +------------------ + +The horizon module is a thorough attempt to make Puppet capable of managing the entirety of horizon. Horizon is a fairly classic django application, which results in a fairly simply Puppet module. + +This module is tested in combination with other modules needed to build and leverage an entire Openstack software stack. These modules can be found, all pulled together in the [openstack module](https://github.com/stackforge/puppet-openstack). + +Setup +----- + +**What the horizon module affects** + +* horizon, the dashboard service for Openstack. + +### Installing horizon + + example% puppet module install puppetlabs/horizon + +### Beginning with horizon + +To utilize the horizon module's functionality you will need to declare multiple resources but you'll find that doing so is much less complicated than the other OpenStack component modules. The following is a modified excerpt from the [openstack module](https://github.com/stackforge/puppet-openstack). We recommend you consult and understand the [openstack module](https://github.com/stackforge/puppet-openstack) and the [core openstack](http://docs.openstack.org) documentation. + +**Define a horizon dashboard** + +```puppet +class { 'memcached': + listen_ip => '127.0.0.1', + tcp_port => '11211', + udp_port => '11211', +} + +class { '::horizon': + cache_server_ip => '127.0.0.1', + cache_server_port => '11211', + secret_key => '12345', + swift => false, + django_debug => 'True', + api_result_limit => '2000', +} +``` + +Implementation +-------------- + +### horizon + +Horizon is a simple module using the combination of a package, template, and the file_line type. Most all the configuration lives inside the included local_settings template and the file_line type is for selectively inserting needed lines into configuration files that aren't explicitly managed by the horizon module. + +Limitations +------------ + +* Only supports Apache using mod_wsgi. + +Development +----------- + +Developer documentation for the entire puppet-openstack project. + +* https://wiki.openstack.org/wiki/Puppet-openstack#Developer_documentation + +Contributors +------------ + +* https://github.com/stackforge/puppet-horizon/graphs/contributors + +Release Notes +------------- + +**4.0.0** + +* Stable Icehouse release. +* Added support to pass extra parameters to vhost. +* Added support to ensure online cache is present and can be refreshed. +* Added support to configure OPENSTACK_HYPERVISOR_FEATURES settings, AVAILABLE_REGIONS, OPENSTACK_NEUTRON_NETWORK. +* Added support to disable configuration of Apache. +* Fixed log ownership and WSGIProcess* settings for Red Hat releases. +* Fixed overriding of policy files in local settings. +* Fixed SSL bugs. +* Improved WSGI configuration. + +**3.1.0** + +* Added option parameterize OPENSTACK_NEUTRON_NETWORK settings. + +**3.0.1** + +* Adds COMPRESS_OFFLINE option to local_settings to fix broken Ubuntu installation. + +**3.0.0** + +* Major release for OpenStack Havana. +* Updated user and group for Debian family OSes. +* Updated policy files for RedHat family OSes. +* Enabled SSL support with cert/key. +* Improved default logging configuration. +* Fixed bug to set LOGOUT_URL properly. +* Introduced new parameters: keystone_url, help_url, endpoint type. +* Fixed user/group regression for Debian. +* Changed keystone_default_role to _member_. + +**2.2.0** + +* Fixed apache 0.9.0 incompatability. +* Various lint fixes. + +**2.1.0** + +* Updated local_settings.py. +* Pinned Apache module version. +* Various lint fixes. + +**2.0.0** + +* Upstream is now part of stackforge. +* httpd config now managed on every platform. +* Provides option to enable Horizon's display of block device mount points. + diff --git a/horizon/Rakefile b/horizon/Rakefile new file mode 100644 index 000000000..4c2b2ed07 --- /dev/null +++ b/horizon/Rakefile @@ -0,0 +1,6 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings = true +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_parameter_defaults') diff --git a/horizon/lib/puppet/parser/functions/os_any2array.rb b/horizon/lib/puppet/parser/functions/os_any2array.rb new file mode 100644 index 000000000..a3877a0bd --- /dev/null +++ b/horizon/lib/puppet/parser/functions/os_any2array.rb @@ -0,0 +1,34 @@ +# +# os_any2array.rb +# +# TODO: Remove this function when puppetlabs-stdlib 4.0.0 is in wider use + +module Puppet::Parser::Functions + newfunction(:os_any2array, :type => :rvalue, :doc => <<-EOS +This converts any object to an array containing that object. Empty argument +lists are converted to an empty array. Arrays are left untouched. Hashes are +converted to arrays of alternating keys and values. + EOS + ) do |arguments| + + if arguments.empty? + return [] + end + + if arguments.length == 1 + if arguments[0].kind_of?(Array) + return arguments[0] + elsif arguments[0].kind_of?(Hash) + result = [] + arguments[0].each do |key, value| + result << key << value + end + return result + end + end + + return arguments + end +end + +# vim: set ts=2 sw=2 et : diff --git a/horizon/manifests/init.pp b/horizon/manifests/init.pp new file mode 100644 index 000000000..985a564d9 --- /dev/null +++ b/horizon/manifests/init.pp @@ -0,0 +1,339 @@ +# == Class: horizon +# +# Installs Horizon dashboard with Apache +# +# === Parameters +# +# [*secret_key*] +# (required) Secret key. This is used by Django to provide cryptographic +# signing, and should be set to a unique, unpredictable value. +# +# [*fqdn*] +# (optional) DEPRECATED, use allowed_hosts and server_aliases instead. +# FQDN(s) used to access Horizon. This is used by Django for +# security reasons. Can be set to * in environments where security is +# deemed unimportant. Also used for Server Aliases in web configs. +# Defaults to ::fqdn +# +# [*servername*] +# (optional) FQDN used for the Server Name directives +# Defaults to ::fqdn. +# +# [*allowed_hosts*] +# (optional) List of hosts which will be set as value of ALLOWED_HOSTS +# parameter in settings_local.py. This is used by Django for +# security reasons. Can be set to * in environments where security is +# deemed unimportant. +# Defaults to ::fqdn. +# +# [*server_aliases*] +# (optional) List of names which should be defined as ServerAlias directives +# in vhost.conf. +# Defaults to ::fqdn. +# +# [*package_ensure*] +# (optional) Package ensure state. Defaults to 'present'. +# +# [*cache_server_ip*] +# (optional) Memcached IP address. Can be a string, or an array. +# Defaults to '127.0.0.1'. +# +# [*cache_server_port*] +# (optional) Memcached port. Defaults to '11211'. +# +# [*swift*] +# (optional) Enable Swift interface extension. Defaults to false. +# +# [*horizon_app_links*] +# (optional) Array of arrays that can be used to add call-out links +# to the dashboard for other apps. There is no specific requirement +# for these apps to be for monitoring, that's just the defacto purpose. +# Each app is defined in two parts, the display name, and +# the URIDefaults to false. Defaults to false. (no app links) +# +# [*keystone_url*] +# (optional) Full url of keystone public endpoint. (Defaults to 'http://127.0.0.1:5000/v2.0') +# Use this parameter in favor of keystone_host, keystone_port and keystone_scheme. +# +# [*keystone_scheme*] +# (optional) DEPRECATED: Use keystone_url instead. +# Scheme of the Keystone service. (Defaults to 'http') +# Setting this parameter overrides keystone_url parameter. +# +# [*keystone_host*] +# (optional) DEPRECATED: Use keystone_url instead. +# IP address of the Keystone service. (Defaults to '127.0.0.1') +# Setting this parameter overrides keystone_url parameter. +# +# [*keystone_port*] +# (optional) DEPRECATED: Use keystone_url instead. +# Port of the Keystone service. (Defaults to 5000) +# Setting this parameter overrides keystone_url parameter. +# +# [*keystone_default_role*] +# (optional) Default Keystone role for new users. Defaults to '_member_'. +# +# [*django_debug*] +# (optional) Enable or disable Django debugging. Defaults to 'False'. +# +# [*openstack_endpoint_type*] +# (optional) endpoint type to use for the endpoints in the Keystone +# service catalog. Defaults to 'undef'. +# +# [*secondary_endpoint_type*] +# (optional) secondary endpoint type to use for the endpoints in the +# Keystone service catalog. Defaults to 'undef'. +# +# [*available_regions*] +# (optional) List of available regions. Value should be a list of tuple: +# [ ['urlOne', 'RegionOne'], ['urlTwo', 'RegionTwo'] ] +# Defaults to undef. +# +# [*api_result_limit*] +# (optional) Maximum number of Swift containers/objects to display +# on a single page. Defaults to 1000. +# +# [*log_level*] +# (optional) Log level. Defaults to 'INFO'. WARNING: Setting this to +# DEBUG will let plaintext passwords be logged in the Horizon log file. +# +# [*local_settings_template*] +# (optional) Location of template to use for local_settings.py generation. +# Defaults to 'horizon/local_settings.py.erb'. +# +# [*help_url*] +# (optional) Location where the documentation should point. +# Defaults to 'http://docs.openstack.org'. +# +# [*compress_offline*] +# (optional) Boolean to enable offline compress of assets. +# Defaults to True +# +# [*hypervisor_options*] +# (optional) A hash of parameters to enable features specific to +# Hypervisors. These include: +# 'can_set_mount_point': Boolean to enable or disable mount point setting +# Defaults to 'True'. +# 'can_set_password': Boolean to enable or disable VM password setting. +# Works only with Xen Hypervisor. +# Defaults to 'False'. +# +# [*neutron_options*] +# (optional) A hash of parameters to enable features specific to +# Neutron. These include: +# 'enable_lb': Boolean to enable or disable Neutron's LBaaS feature. +# Defaults to False. +# 'enable_firewall': Boolean to enable or disable Neutron's FWaaS feature. +# Defaults to False. +# 'enable_quotas': Boolean to enable or disable Neutron quotas. +# Defaults to True. +# 'enable_security_group': Boolean to enable or disable Neutron +# security groups. Defaults to True. +# 'enable_vpn': Boolean to enable or disable Neutron's VPNaaS feature. +# Defaults to False. +# 'profile_support': A string indiciating which plugin-specific +# profiles to enable. Defaults to 'None', other options include +# 'cisco'. +# +# [*configure_apache*] +# (optional) Configure Apache for Horizon. (Defaults to true) +# +# [*bind_address*] +# (optional) Bind address in Apache for Horizon. (Defaults to undef) +# +# [*listen_ssl*] +# (optional) Enable SSL support in Apache. (Defaults to false) +# +# [*ssl_redirect*] +# (optional) Whether to redirect http to https +# Defaults to True +# +# [*horizon_cert*] +# (required with listen_ssl) Certificate to use for SSL support. +# +# [*horizon_key*] +# (required with listen_ssl) Private key to use for SSL support. +# +# [*horizon_ca*] +# (required with listen_ssl) CA certificate to use for SSL support. +# +# [*vhost_extra_params*] +# (optionnal) extra parameter to pass to the apache::vhost class +# Defaults to undef +# +# [*file_upload_temp_dir*] +# (optional) Location to use for temporary storage of images uploaded +# You must ensure that the path leading to the directory is created +# already, only the last level directory is created by this manifest. +# Specify an absolute pathname. +# Defaults to /tmp +# +# [*secure_cookies*] +# (optional) Enables security settings for cookies. Useful when using +# https on public sites. See: http://docs.openstack.org/developer/horizon/topics/deployment.html#secure-site-recommendations +# Defaults to false +# +# === Deprecation notes +# +# If any value is provided for keystone_scheme, keystone_host, or +# keystone_port parameters; keystone_url will be completely ignored. Also +# can_set_mount_point is deprecated. +# +# === Examples +# +# class { 'horizon': +# secret => 's3cr3t', +# keystone_url => 'https://10.0.0.10:5000/v2.0', +# available_regions => [ +# ['http://region-1.example.com:5000/v2.0', 'Region-1'], +# ['http://region-2.example.com:5000/v2.0', 'Region-2'] +# ] +# } +# +class horizon( + $secret_key, + $fqdn = undef, + $package_ensure = 'present', + $cache_server_ip = '127.0.0.1', + $cache_server_port = '11211', + $swift = false, + $horizon_app_links = false, + $keystone_url = 'http://127.0.0.1:5000/v2.0', + $keystone_default_role = '_member_', + $django_debug = 'False', + $openstack_endpoint_type = undef, + $secondary_endpoint_type = undef, + $available_regions = undef, + $api_result_limit = 1000, + $log_level = 'INFO', + $help_url = 'http://docs.openstack.org', + $local_settings_template = 'horizon/local_settings.py.erb', + $configure_apache = true, + $bind_address = undef, + $servername = $::fqdn, + $server_aliases = $::fqdn, + $allowed_hosts = $::fqdn, + $listen_ssl = false, + $ssl_redirect = true, + $horizon_cert = undef, + $horizon_key = undef, + $horizon_ca = undef, + $compress_offline = 'True', + $hypervisor_options = {}, + $neutron_options = {}, + $file_upload_temp_dir = '/tmp', + # DEPRECATED PARAMETERS + $can_set_mount_point = undef, + $keystone_host = undef, + $keystone_port = undef, + $keystone_scheme = undef, + $vhost_extra_params = undef, + $secure_cookies = false, +) { + + include ::horizon::params + + if $swift { + warning('swift parameter is deprecated and has no effect.') + } + + if $keystone_scheme { + warning('The keystone_scheme parameter is deprecated, use keystone_url instead.') + } + + if $keystone_host { + warning('The keystone_host parameter is deprecated, use keystone_url instead.') + } + + if $keystone_port { + warning('The keystone_port parameter is deprecated, use keystone_url instead.') + } + + # Default options for the OPENSTACK_HYPERVISOR_FEATURES section. These will + # be merged with user-provided options when the local_settings.py.erb + # template is interpolated. Also deprecates can_set_mount_point. + if $can_set_mount_point { + warning('The can_set_mount_point parameter is deprecated, use hypervisor_options instead.') + $hypervisor_defaults = { + 'can_set_mount_point' => $can_set_mount_point, + 'can_set_password' => false + } + } else { + $hypervisor_defaults = { + 'can_set_mount_point' => true, + 'can_set_password' => false + } + } + + if $fqdn { + warning('Parameter fqdn is deprecated. Please use parameter allowed_hosts for setting ALLOWED_HOSTS in settings_local.py and parameter server_aliases for setting ServerAlias directives in vhost.conf.') + $final_allowed_hosts = $fqdn + $final_server_aliases = $fqdn + } else { + $final_allowed_hosts = $allowed_hosts + $final_server_aliases = $server_aliases + } + + + # Default options for the OPENSTACK_NEUTRON_NETWORK section. These will + # be merged with user-provided options when the local_settings.py.erb + # template is interpolated. + $neutron_defaults = { + 'enable_lb' => false, + 'enable_firewall' => false, + 'enable_quotas' => true, + 'enable_security_group' => true, + 'enable_vpn' => false, + 'profile_support' => 'None' + } + + Service <| title == 'memcached' |> -> Class['horizon'] + + package { 'horizon': + ensure => $package_ensure, + name => $::horizon::params::package_name, + } + package { 'python-lesscpy': + ensure => $package_ensure, + } + + exec { 'refresh_horizon_django_cache': + command => "${::horizon::params::manage_py} compress", + refreshonly => true, + subscribe => File[$::horizon::params::config_file], + require => [Package['python-lesscpy'], Package['horizon']], + } + + if $compress_offline { + file { $::horizon::params::config_file: + content => template($local_settings_template), + mode => '0644', + notify => Exec['refresh_horizon_django_cache'], + require => Package['horizon'], + } + } + + if $configure_apache { + class { 'horizon::wsgi::apache': + bind_address => $bind_address, + servername => $servername, + server_aliases => $final_server_aliases, + listen_ssl => $listen_ssl, + ssl_redirect => $ssl_redirect, + horizon_cert => $horizon_cert, + horizon_key => $horizon_key, + horizon_ca => $horizon_ca, + extra_params => $vhost_extra_params, + } + } + + if $file_upload_temp_dir != '/tmp' { + file { $file_upload_temp_dir : + ensure => directory, + owner => $::horizon::params::wsgi_user, + group => $::horizon::params::wsgi_group, + mode => '0755' + } + } + +} diff --git a/horizon/manifests/params.pp b/horizon/manifests/params.pp new file mode 100644 index 000000000..67b44ac8f --- /dev/null +++ b/horizon/manifests/params.pp @@ -0,0 +1,46 @@ +# these parameters need to be accessed from several locations and +# should be considered to be constant +class horizon::params { + + $logdir = '/var/log/horizon' + $django_wsgi = '/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi' + $manage_py = '/usr/share/openstack-dashboard/manage.py' + + case $::osfamily { + 'RedHat': { + $http_service = 'httpd' + $http_modwsgi = 'mod_wsgi' + $package_name = 'openstack-dashboard' + $config_file = '/etc/openstack-dashboard/local_settings' + $httpd_config_file = '/etc/httpd/conf.d/openstack-dashboard.conf' + $httpd_listen_config_file = '/etc/httpd/conf/httpd.conf' + $root_url = '/dashboard' + $apache_user = 'apache' + $apache_group = 'apache' + $wsgi_user = 'dashboard' + $wsgi_group = 'dashboard' + } + 'Debian': { + $http_service = 'apache2' + $config_file = '/etc/openstack-dashboard/local_settings.py' + $httpd_config_file = '/etc/apache2/conf.d/openstack-dashboard.conf' + $httpd_listen_config_file = '/etc/apache2/ports.conf' + $root_url = '/horizon' + $apache_user = 'www-data' + $apache_group = 'www-data' + $wsgi_user = 'horizon' + $wsgi_group = 'horizon' + case $::operatingsystem { + 'Debian': { + $package_name = 'openstack-dashboard-apache' + } + default: { + $package_name = 'openstack-dashboard' + } + } + } + default: { + fail("Unsupported osfamily: ${::osfamily} operatingsystem: ${::operatingsystem}, module ${module_name} only support osfamily RedHat and Debian") + } + } +} diff --git a/horizon/manifests/wsgi/apache.pp b/horizon/manifests/wsgi/apache.pp new file mode 100644 index 000000000..3af6ca0c3 --- /dev/null +++ b/horizon/manifests/wsgi/apache.pp @@ -0,0 +1,194 @@ +# == Class: horizon::wsgi::apache +# +# Configures Apache WSGI for Horizon. +# +# === Parameters +# +# [*bind_address*] +# (optional) Bind address in Apache for Horizon. (Defaults to '0.0.0.0') +# +# [*server_aliases*] +# (optional) List of names which should be defined as ServerAlias directives +# in vhost.conf. +# Defaults to ::fqdn. +# +# [*listen_ssl*] +# (optional) Enable SSL support in Apache. (Defaults to false) +# +# [*horizon_cert*] +# (required with listen_ssl) Certificate to use for SSL support. +# +# [*horizon_key*] +# (required with listen_ssl) Private key to use for SSL support. +# +# [*horizon_ca*] +# (required with listen_ssl) CA certificate to use for SSL support. +# +# [*wsgi_processes*] +# (optional) Number of Horizon processes to spawn +# Defaults to '3' +# +# [*wsgi_threads*] +# (optional) Number of thread to run in a Horizon process +# Defaults to '10' +# +# [*priority*] +# (optional) The apache vhost priority. +# Defaults to '15'. To set Horizon as the primary vhost, change to '10'. +# +# [*extra_params*] +# (optional) A hash of extra paramaters for apache::wsgi class. +# Defaults to {} +class horizon::wsgi::apache ( + $bind_address = undef, + $fqdn = undef, + $servername = $::fqdn, + $server_aliases = $::fqdn, + $listen_ssl = false, + $ssl_redirect = true, + $horizon_cert = undef, + $horizon_key = undef, + $horizon_ca = undef, + $wsgi_processes = '3', + $wsgi_threads = '10', + $priority = '15', + $vhost_conf_name = 'horizon_vhost', + $vhost_ssl_conf_name = 'horizon_ssl_vhost', + $extra_params = {}, +) { + + include ::horizon::params + include ::apache + + if $fqdn { + warning('Parameter fqdn is deprecated. Please use parameter server_aliases for setting ServerAlias directives in vhost.conf.') + $final_server_aliases = $fqdn + } else { + $final_server_aliases = $server_aliases + } + + if $::osfamily == 'RedHat' { + class { 'apache::mod::wsgi': + wsgi_socket_prefix => '/var/run/wsgi' + } + } else { + include ::apache::mod::wsgi + } + + # We already use apache::vhost to generate our own + # configuration file, let's clean the configuration + # embedded within the package + file { $::horizon::params::httpd_config_file: + ensure => present, + content => "# +# This file has been cleaned by Puppet. +# +# OpenStack Horizon configuration has been moved to: +# - ${priority}-${vhost_conf_name}.conf +# - ${priority}-${vhost_ssl_conf_name}.conf +#", + require => Package[$::horizon::params::package_name] + } + + + if $listen_ssl { + include ::apache::mod::ssl + $ensure_ssl_vhost = 'present' + + if $horizon_ca == undef { + fail('The horizon_ca parameter is required when listen_ssl is true') + } + + if $horizon_cert == undef { + fail('The horizon_cert parameter is required when listen_ssl is true') + } + + if $horizon_key == undef { + fail('The horizon_key parameter is required when listen_ssl is true') + } + + if $ssl_redirect { + $redirect_match = '(.*)' + $redirect_url = "https://${servername}" + } + + } else { + $ensure_ssl_vhost = 'absent' + $redirect_match = '^/$' + $redirect_url = $::horizon::params::root_url + } + + Package['horizon'] -> Package[$::horizon::params::http_service] + File[$::horizon::params::config_file] ~> Service[$::horizon::params::http_service] + + $unix_user = $::osfamily ? { + 'RedHat' => $::horizon::params::apache_user, + default => $::horizon::params::wsgi_user + } + $unix_group = $::osfamily ? { + 'RedHat' => $::horizon::params::apache_group, + default => $::horizon::params::wsgi_group, + } + + file { $::horizon::params::logdir: + ensure => directory, + owner => $unix_user, + group => $unix_group, + before => Service[$::horizon::params::http_service], + mode => '0751', + require => Package['horizon'] + } + + file { "${::horizon::params::logdir}/horizon.log": + ensure => file, + owner => $unix_user, + group => $unix_group, + before => Service[$::horizon::params::http_service], + mode => '0640', + require => [ File[$::horizon::params::logdir], Package['horizon'] ], + } + + $default_vhost_conf = { + ip => $bind_address, + servername => $servername, + serveraliases => os_any2array($final_server_aliases), + docroot => '/var/www/', + access_log_file => 'horizon_access.log', + error_log_file => 'horizon_error.log', + priority => $priority, + aliases => [ + { alias => '/static', path => '/usr/share/openstack-dashboard/static' } + ], + port => 80, + ssl_cert => $horizon_cert, + ssl_key => $horizon_key, + ssl_ca => $horizon_ca, + wsgi_script_aliases => hash([$::horizon::params::root_url, $::horizon::params::django_wsgi]), + wsgi_daemon_process => $::horizon::params::wsgi_group, + wsgi_daemon_process_options => { + processes => $wsgi_processes, + threads => $wsgi_threads, + user => $unix_user, + group => $unix_group, + }, + wsgi_import_script => $::horizon::params::django_wsgi, + wsgi_process_group => $::horizon::params::wsgi_group, + redirectmatch_status => 'permanent', + } + + ensure_resource('apache::vhost', $vhost_conf_name, merge ($default_vhost_conf, $extra_params, { + redirectmatch_regexp => "${redirect_match} ${redirect_url}", + })) + ensure_resource('apache::vhost', $vhost_ssl_conf_name, merge ($default_vhost_conf, $extra_params, { + access_log_file => 'horizon_ssl_access.log', + error_log_file => 'horizon_ssl_error.log', + priority => $priority, + ssl => true, + port => 443, + ensure => $ensure_ssl_vhost, + wsgi_daemon_process => 'horizon-ssl', + wsgi_process_group => 'horizon-ssl', + redirectmatch_regexp => "^/$ ${::horizon::params::root_url}" + })) + +} diff --git a/horizon/spec/classes/horizon_init_spec.rb b/horizon/spec/classes/horizon_init_spec.rb new file mode 100644 index 000000000..3bd7c9297 --- /dev/null +++ b/horizon/spec/classes/horizon_init_spec.rb @@ -0,0 +1,283 @@ +require 'spec_helper' + +describe 'horizon' do + + let :params do + { 'secret_key' => 'elj1IWiLoWHgcyYxFVLj7cM5rGOOxWl0', + 'fqdn' => '*' } + end + + let :pre_condition do + 'include apache' + end + + let :fixtures_path do + File.expand_path(File.join(__FILE__, '..', '..', 'fixtures')) + end + + let :facts do + { :concat_basedir => '/var/lib/puppet/concat', + :fqdn => 'some.host.tld' + } + end + + shared_examples 'horizon' do + + context 'with default parameters' do + it { + should contain_package('python-lesscpy').with_ensure('present') + should contain_package('horizon').with_ensure('present') + } + it { should contain_exec('refresh_horizon_django_cache').with({ + :command => '/usr/share/openstack-dashboard/manage.py compress', + :refreshonly => true, + })} + + it 'configures apache' do + should contain_class('horizon::wsgi::apache').with({ + :servername => 'some.host.tld', + :listen_ssl => false, + :servername => 'some.host.tld', + :extra_params => {}, + }) + end + + it 'generates local_settings.py' do + verify_contents(subject, platforms_params[:config_file], [ + 'DEBUG = False', + "ALLOWED_HOSTS = ['*', ]", + "SECRET_KEY = 'elj1IWiLoWHgcyYxFVLj7cM5rGOOxWl0'", + 'OPENSTACK_KEYSTONE_URL = "http://127.0.0.1:5000/v2.0"', + 'OPENSTACK_KEYSTONE_DEFAULT_ROLE = "_member_"', + " 'can_set_mount_point': True,", + " 'can_set_password': False,", + " 'enable_lb': False,", + " 'enable_firewall': False,", + " 'enable_quotas': True,", + " 'enable_security_group': True,", + " 'enable_vpn': False,", + 'API_RESULT_LIMIT = 1000', + "LOGIN_URL = '#{platforms_params[:root_url]}/auth/login/'", + "LOGOUT_URL = '#{platforms_params[:root_url]}/auth/logout/'", + "LOGIN_REDIRECT_URL = '#{platforms_params[:root_url]}'", + 'COMPRESS_OFFLINE = True', + "FILE_UPLOAD_TEMP_DIR = '/tmp'" + ]) + end + end + + context 'with overridden parameters' do + before do + params.merge!({ + :cache_server_ip => '10.0.0.1', + :keystone_default_role => 'SwiftOperator', + :keystone_url => 'https://keystone.example.com:4682', + :openstack_endpoint_type => 'internalURL', + :secondary_endpoint_type => 'ANY-VALUE', + :django_debug => true, + :api_result_limit => 4682, + :compress_offline => 'False', + :hypervisor_options => {'can_set_mount_point' => false, 'can_set_password' => true }, + :neutron_options => {'enable_lb' => true, 'enable_firewall' => true, 'enable_quotas' => false, 'enable_security_group' => false, 'enable_vpn' => true, 'profile_support' => 'cisco' }, + :file_upload_temp_dir => '/var/spool/horizon', + :secure_cookies => true + }) + end + + it 'generates local_settings.py' do + verify_contents(subject, platforms_params[:config_file], [ + 'DEBUG = True', + "ALLOWED_HOSTS = ['*', ]", + 'CSRF_COOKIE_SECURE = True', + 'SESSION_COOKIE_SECURE = True', + "SECRET_KEY = 'elj1IWiLoWHgcyYxFVLj7cM5rGOOxWl0'", + " 'LOCATION': '10.0.0.1:11211',", + 'OPENSTACK_KEYSTONE_URL = "https://keystone.example.com:4682"', + 'OPENSTACK_KEYSTONE_DEFAULT_ROLE = "SwiftOperator"', + " 'can_set_mount_point': False,", + " 'can_set_password': True,", + " 'enable_lb': True,", + " 'enable_firewall': True,", + " 'enable_quotas': False,", + " 'enable_security_group': False,", + " 'enable_vpn': True,", + " 'profile_support': 'cisco',", + 'OPENSTACK_ENDPOINT_TYPE = "internalURL"', + 'SECONDARY_ENDPOINT_TYPE = "ANY-VALUE"', + 'API_RESULT_LIMIT = 4682', + 'COMPRESS_OFFLINE = False', + "FILE_UPLOAD_TEMP_DIR = '/var/spool/horizon'", + ]) + end + + it { should contain_exec('refresh_horizon_django_cache') } + end + + context 'with overridden parameters and cache_server_ip array' do + before do + params.merge!({ + :cache_server_ip => ['10.0.0.1','10.0.0.2'], + }) + end + + it 'generates local_settings.py' do + verify_contents(subject, platforms_params[:config_file], [ + " 'LOCATION': [ '10.0.0.1:11211','10.0.0.2:11211', ],", + ]) + end + + it { should contain_exec('refresh_horizon_django_cache') } + end + + context 'with deprecated parameters' do + before do + params.merge!({ + :keystone_host => 'keystone.example.com', + :keystone_port => 4682, + :keystone_scheme => 'https', + :can_set_mount_point => true, + }) + end + + it 'generates local_settings.py' do + verify_contents(subject, platforms_params[:config_file], [ + 'OPENSTACK_KEYSTONE_URL = "https://keystone.example.com:4682/v2.0"', + " 'can_set_mount_point': True," + ]) + end + end + + context 'with vhost_extra_params' do + before do + params.merge!({ + :vhost_extra_params => { 'add_listen' => false }, + }) + end + + it 'configures apache' do + should contain_class('horizon::wsgi::apache').with({ + :extra_params => { 'add_listen' => false }, + }) + end + end + + + context 'with ssl enabled' do + before do + params.merge!({ + :listen_ssl => true, + :servername => 'some.host.tld', + :horizon_cert => '/etc/pki/tls/certs/httpd.crt', + :horizon_key => '/etc/pki/tls/private/httpd.key', + :horizon_ca => '/etc/pki/tls/certs/ca.crt', + }) + end + + it 'configures apache' do + should contain_class('horizon::wsgi::apache').with({ + :bind_address => nil, + :listen_ssl => true, + :horizon_cert => '/etc/pki/tls/certs/httpd.crt', + :horizon_key => '/etc/pki/tls/private/httpd.key', + :horizon_ca => '/etc/pki/tls/certs/ca.crt', + }) + end + end + + context 'without apache' do + before do + params.merge!({ :configure_apache => false }) + end + + it 'does not configure apache' do + should_not contain_class('horizon::wsgi::apache') + end + end + + context 'with available_regions parameter' do + before do + params.merge!({ + :available_regions => [ + ['http://region-1.example.com:5000/v2.0', 'Region-1'], + ['http://region-2.example.com:5000/v2.0', 'Region-2'] + ] + }) + end + + it 'AVAILABLE_REGIONS is configured' do + verify_contents(subject, platforms_params[:config_file], [ + "AVAILABLE_REGIONS = [", + " ('http://region-1.example.com:5000/v2.0', 'Region-1'),", + " ('http://region-2.example.com:5000/v2.0', 'Region-2'),", + "]" + ]) + end + end + + context 'with overriding local_settings_template' do + before do + params.merge!({ + :django_debug => 'True', + :help_url => 'https://docs.openstack.org', + :local_settings_template => fixtures_path + '/override_local_settings.py.erb' + }) + end + + it 'uses the custom local_settings.py template' do + verify_contents(subject, platforms_params[:config_file], [ + '# Custom local_settings.py', + 'DEBUG = True', + "HORIZON_CONFIG = {", + " 'dashboards': ('project', 'admin', 'settings',),", + " 'default_dashboard': 'project',", + " 'user_home': 'openstack_dashboard.views.get_user_home',", + " 'ajax_queue_limit': 10,", + " 'auto_fade_alerts': {", + " 'delay': 3000,", + " 'fade_duration': 1500,", + " 'types': ['alert-success', 'alert-info']", + " },", + " 'help_url': \"https://docs.openstack.org\",", + " 'exceptions': {'recoverable': exceptions.RECOVERABLE,", + " 'not_found': exceptions.NOT_FOUND,", + " 'unauthorized': exceptions.UNAUTHORIZED},", + "}", + ]) + end + end + end + + context 'on RedHat platforms' do + before do + facts.merge!({ + :osfamily => 'RedHat', + :operatingsystemrelease => '6.0' + }) + end + + let :platforms_params do + { :config_file => '/etc/openstack-dashboard/local_settings', + :package_name => 'openstack-dashboard', + :root_url => '/dashboard' } + end + + it_behaves_like 'horizon' + end + + context 'on Debian platforms' do + before do + facts.merge!({ + :osfamily => 'Debian', + :operatingsystemrelease => '6.0' + }) + end + + let :platforms_params do + { :config_file => '/etc/openstack-dashboard/local_settings.py', + :package_name => 'openstack-dashboard-apache', + :root_url => '/horizon' } + end + + it_behaves_like 'horizon' + end +end diff --git a/horizon/spec/classes/horizon_wsgi_apache_spec.rb b/horizon/spec/classes/horizon_wsgi_apache_spec.rb new file mode 100644 index 000000000..68633fbab --- /dev/null +++ b/horizon/spec/classes/horizon_wsgi_apache_spec.rb @@ -0,0 +1,223 @@ +require 'spec_helper' + +describe 'horizon::wsgi::apache' do + + let :params do + { :fqdn => '*', + :servername => 'some.host.tld', + :wsgi_processes => '3', + :wsgi_threads => '10', + } + end + + let :pre_condition do + "include apache\n" + + "class { 'horizon': secret_key => 's3cr3t', configure_apache => false }" + end + + let :fixtures_path do + File.expand_path(File.join(__FILE__, '..', '..', 'fixtures')) + end + + let :facts do + { :concat_basedir => '/var/lib/puppet/concat', + :fqdn => 'some.host.tld' + } + end + + shared_examples 'apache for horizon' do + + context 'with default parameters' do + it 'configures apache' do + should contain_class('horizon::params') + should contain_class('apache') + should contain_class('apache::mod::wsgi') + should contain_service('httpd').with_name(platforms_params[:http_service]) + should contain_file(platforms_params[:httpd_config_file]) + should contain_package('horizon').with_ensure('present') + should contain_apache__vhost('horizon_vhost').with( + 'servername' => 'some.host.tld', + 'access_log_file' => 'horizon_access.log', + 'error_log_file' => 'horizon_error.log', + 'priority' => '15', + 'serveraliases' => '*', + 'docroot' => '/var/www/', + 'ssl' => 'false', + 'redirectmatch_status' => 'permanent', + 'redirectmatch_regexp' => "^/$ #{platforms_params[:root_url]}", + 'wsgi_script_aliases' => { platforms_params[:root_url] => '/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi' }, + 'wsgi_process_group' => platforms_params[:wsgi_group], + 'wsgi_daemon_process' => platforms_params[:wsgi_group], + 'wsgi_daemon_process_options' => { 'processes' => params[:wsgi_processes], 'threads' => params[:wsgi_threads], 'user' => platforms_params[:unix_user], 'group' => platforms_params[:unix_group] } + ) + end + end + + context 'with overriden parameters' do + before do + params.merge!({ + :priority => '10', + }) + end + + it 'configures apache' do + should contain_class('horizon::params') + should contain_class('apache') + should contain_class('apache::mod::wsgi') + should contain_service('httpd').with_name(platforms_params[:http_service]) + should contain_file(platforms_params[:httpd_config_file]) + should contain_package('horizon').with_ensure('present') + should contain_apache__vhost('horizon_vhost').with( + 'servername' => 'some.host.tld', + 'access_log_file' => 'horizon_access.log', + 'error_log_file' => 'horizon_error.log', + 'priority' => params[:priority], + 'serveraliases' => '*', + 'docroot' => '/var/www/', + 'ssl' => 'false', + 'redirectmatch_status' => 'permanent', + 'redirectmatch_regexp' => "^/$ #{platforms_params[:root_url]}", + 'wsgi_script_aliases' => { platforms_params[:root_url] => '/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi' }, + 'wsgi_process_group' => platforms_params[:wsgi_group], + 'wsgi_daemon_process' => platforms_params[:wsgi_group], + 'wsgi_daemon_process_options' => { 'processes' => params[:wsgi_processes], 'threads' => params[:wsgi_threads], 'user' => platforms_params[:unix_user], 'group' => platforms_params[:unix_group] } + ) + end + end + + context 'with ssl enabled' do + before do + params.merge!({ + :listen_ssl => true, + :ssl_redirect => true, + :horizon_cert => '/etc/pki/tls/certs/httpd.crt', + :horizon_key => '/etc/pki/tls/private/httpd.key', + :horizon_ca => '/etc/pki/tls/certs/ca.crt', + }) + end + + context 'with required parameters' do + it 'configures apache for SSL' do + should contain_class('apache::mod::ssl') + end + it { should contain_apache__vhost('horizon_ssl_vhost').with( + 'servername' => 'some.host.tld', + 'access_log_file' => 'horizon_ssl_access.log', + 'error_log_file' => 'horizon_ssl_error.log', + 'priority' => '15', + 'serveraliases' => '*', + 'docroot' => '/var/www/', + 'ssl' => 'true', + 'ssl_cert' => '/etc/pki/tls/certs/httpd.crt', + 'ssl_key' => '/etc/pki/tls/private/httpd.key', + 'ssl_ca' => '/etc/pki/tls/certs/ca.crt', + 'redirectmatch_status' => 'permanent', + 'redirectmatch_regexp' => "^/$ #{platforms_params[:root_url]}", + 'wsgi_process_group' => 'horizon-ssl', + 'wsgi_daemon_process' => 'horizon-ssl', + 'wsgi_script_aliases' => { platforms_params[:root_url] => '/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi' } + )} + + it { should contain_apache__vhost('horizon_vhost').with( + 'servername' => 'some.host.tld', + 'access_log_file' => 'horizon_access.log', + 'error_log_file' => 'horizon_error.log', + 'priority' => '15', + 'serveraliases' => '*', + 'docroot' => '/var/www/', + 'ssl' => 'false', + 'redirectmatch_status' => 'permanent', + 'redirectmatch_regexp' => '(.*) https://some.host.tld', + 'wsgi_process_group' => platforms_params[:wsgi_group], + 'wsgi_daemon_process' => platforms_params[:wsgi_group], + 'wsgi_script_aliases' => { platforms_params[:root_url] => '/usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi' } + )} + end + + context 'without required parameters' do + + context 'without horizon_ca parameter' do + before { params.delete(:horizon_ca) } + it_raises 'a Puppet::Error', /The horizon_ca parameter is required when listen_ssl is true/ + end + + context 'without horizon_cert parameter' do + before { params.delete(:horizon_cert) } + it_raises 'a Puppet::Error', /The horizon_cert parameter is required when listen_ssl is true/ + end + + context 'without horizon_key parameter' do + before { params.delete(:horizon_key) } + it_raises 'a Puppet::Error', /The horizon_key parameter is required when listen_ssl is true/ + end + end + + context 'with extra parameters' do + before do + params.merge!({ + :extra_params => { + 'add_listen' => false, + 'docroot' => '/tmp' + }, + }) + end + + it 'configures apache' do + should contain_apache__vhost('horizon_vhost').with( + 'add_listen' => false, + 'docroot' => '/tmp' + ) + end + + end + + + end + end + + context 'on RedHat platforms' do + before do + facts.merge!({ + :osfamily => 'RedHat', + :operatingsystemrelease => '6.0' + }) + end + + let :platforms_params do + { :http_service => 'httpd', + :httpd_config_file => '/etc/httpd/conf.d/openstack-dashboard.conf', + :root_url => '/dashboard', + :apache_user => 'apache', + :apache_group => 'apache', + :wsgi_user => 'dashboard', + :wsgi_group => 'dashboard', + :unix_user => 'apache', + :unix_group => 'apache' } + end + + it_behaves_like 'apache for horizon' + end + + context 'on Debian platforms' do + before do + facts.merge!({ + :osfamily => 'Debian', + :operatingsystemrelease => '6.0' + }) + end + + let :platforms_params do + { :http_service => 'apache2', + :httpd_config_file => '/etc/apache2/conf.d/openstack-dashboard.conf', + :root_url => '/horizon', + :apache_user => 'www-data', + :apache_group => 'www-data', + :wsgi_user => 'horizon', + :wsgi_group => 'horizon', + :unix_user => 'horizon', + :unix_group => 'horizon' } + end + + it_behaves_like 'apache for horizon' + end +end diff --git a/horizon/spec/fixtures/override_local_settings.py.erb b/horizon/spec/fixtures/override_local_settings.py.erb new file mode 100644 index 000000000..42639652a --- /dev/null +++ b/horizon/spec/fixtures/override_local_settings.py.erb @@ -0,0 +1,18 @@ +# Custom local_settings.py +DEBUG = <%= @django_debug %> + +HORIZON_CONFIG = { + 'dashboards': ('project', 'admin', 'settings',), + 'default_dashboard': 'project', + 'user_home': 'openstack_dashboard.views.get_user_home', + 'ajax_queue_limit': 10, + 'auto_fade_alerts': { + 'delay': 3000, + 'fade_duration': 1500, + 'types': ['alert-success', 'alert-info'] + }, + 'help_url': "<%= @help_url %>", + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, +} diff --git a/horizon/spec/shared_examples.rb b/horizon/spec/shared_examples.rb new file mode 100644 index 000000000..51e11c0ba --- /dev/null +++ b/horizon/spec/shared_examples.rb @@ -0,0 +1,5 @@ +shared_examples_for "a Puppet::Error" do |description| + it "with message matching #{description.inspect}" do + expect { subject }.to raise_error(Puppet::Error, description) + end +end diff --git a/horizon/spec/spec_helper.rb b/horizon/spec/spec_helper.rb new file mode 100644 index 000000000..53d4dd02d --- /dev/null +++ b/horizon/spec/spec_helper.rb @@ -0,0 +1,7 @@ +require 'puppetlabs_spec_helper/module_spec_helper' +require 'shared_examples' + +RSpec.configure do |c| + c.alias_it_should_behave_like_to :it_configures, 'configures' + c.alias_it_should_behave_like_to :it_raises, 'raises' +end diff --git a/horizon/spec/unit/puppet/parser/functions/os_any2array_spec.rb b/horizon/spec/unit/puppet/parser/functions/os_any2array_spec.rb new file mode 100644 index 000000000..dd5bbe2c3 --- /dev/null +++ b/horizon/spec/unit/puppet/parser/functions/os_any2array_spec.rb @@ -0,0 +1,55 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the os_any2array function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("os_any2array").should == "function_os_any2array" + end + + it "should return an empty array if there is less than 1 argument" do + result = scope.function_os_any2array([]) + result.should(eq([])) + end + + it "should convert boolean true to [ true ] " do + result = scope.function_os_any2array([true]) + result.should(eq([true])) + end + + it "should convert one object to [object]" do + result = scope.function_os_any2array(['one']) + result.should(eq(['one'])) + end + + it "should convert multiple objects to [objects]" do + result = scope.function_os_any2array(['one', 'two']) + result.should(eq(['one', 'two'])) + end + + it "should return empty array it was called with" do + result = scope.function_os_any2array([[]]) + result.should(eq([])) + end + + it "should return one-member array it was called with" do + result = scope.function_os_any2array([['string']]) + result.should(eq(['string'])) + end + + it "should return multi-member array it was called with" do + result = scope.function_os_any2array([['one', 'two']]) + result.should(eq(['one', 'two'])) + end + + it "should return members of a hash it was called with" do + result = scope.function_os_any2array([{ 'key' => 'value' }]) + result.should(eq(['key', 'value'])) + end + + it "should return an empty array if it was called with an empty hash" do + result = scope.function_os_any2array([{ }]) + result.should(eq([])) + end +end diff --git a/horizon/templates/local_settings.py.erb b/horizon/templates/local_settings.py.erb new file mode 100644 index 000000000..e2b10de30 --- /dev/null +++ b/horizon/templates/local_settings.py.erb @@ -0,0 +1,555 @@ +import os + +from django.utils.translation import ugettext_lazy as _ + +from openstack_dashboard import exceptions + +DEBUG = <%= @django_debug.to_s.capitalize %> +TEMPLATE_DEBUG = DEBUG + +# Required for Django 1.5. +# If horizon is running in production (DEBUG is False), set this +# with the list of host/domain names that the application can serve. +# For more information see: +# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +#ALLOWED_HOSTS = ['horizon.example.com', ] +<% if @final_allowed_hosts.kind_of?(Array) %> +ALLOWED_HOSTS = ['<%= @final_allowed_hosts.join("', '") %>', ] +<% else %> +ALLOWED_HOSTS = ['<%= @final_allowed_hosts %>', ] +<% end %> + +# Set SSL proxy settings: +# For Django 1.4+ pass this header from the proxy after terminating the SSL, +# and don't forget to strip it from the client's request. +# For more information see: +# https://docs.djangoproject.com/en/1.4/ref/settings/#secure-proxy-ssl-header +# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') + +# If Horizon is being served through SSL, then uncomment the following two +# settings to better secure the cookies from security exploits +<% if @secure_cookies %> +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True +<% else %> +#CSRF_COOKIE_SECURE = True +#SESSION_COOKIE_SECURE = True +<% end %> + +# Overrides for OpenStack API versions. Use this setting to force the +# OpenStack dashboard to use a specfic API version for a given service API. +# NOTE: The version should be formatted as it appears in the URL for the +# service API. For example, The identity service APIs have inconsistent +# use of the decimal point, so valid options would be "2.0" or "3". +# OPENSTACK_API_VERSIONS = { +# "identity": 3 +# } + +# Set this to True if running on multi-domain model. When this is enabled, it +# will require user to enter the Domain name in addition to username for login. +# OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = False + +# Overrides the default domain used when running on single-domain model +# with Keystone V3. All entities will be created in the default domain. +# OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'Default' + +# Set Console type: +# valid options would be "AUTO", "VNC" or "SPICE" +# CONSOLE_TYPE = "AUTO" + +# Default OpenStack Dashboard configuration. +HORIZON_CONFIG = { + 'dashboards': ('project', 'admin', 'settings',), + 'default_dashboard': 'project', + 'user_home': 'openstack_dashboard.views.get_user_home', + 'ajax_queue_limit': 10, + 'auto_fade_alerts': { + 'delay': 3000, + 'fade_duration': 1500, + 'types': ['alert-success', 'alert-info'] + }, + 'help_url': "<%= @help_url %>", + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, +} + +# Specify a regular expression to validate user passwords. +# HORIZON_CONFIG["password_validator"] = { +# "regex": '.*', +# "help_text": _("Your password does not meet the requirements.") +# } + +# Disable simplified floating IP address management for deployments with +# multiple floating IP pools or complex network requirements. +# HORIZON_CONFIG["simple_ip_management"] = False + +# Turn off browser autocompletion for the login form if so desired. +# HORIZON_CONFIG["password_autocomplete"] = "off" + +LOCAL_PATH = os.path.dirname(os.path.abspath(__file__)) + +# Set custom secret key: +# You can either set it to a specific value or you can let horizion generate a +# default secret key that is unique on this machine, e.i. regardless of the +# amount of Python WSGI workers (if used behind Apache+mod_wsgi): However, there +# may be situations where you would want to set this explicitly, e.g. when +# multiple dashboard instances are distributed on different machines (usually +# behind a load-balancer). Either you have to make sure that a session gets all +# requests routed to the same dashboard instance or you set the same SECRET_KEY +# for all of them. +# from horizon.utils import secret_key +# SECRET_KEY = secret_key.generate_or_read_from_file(os.path.join(LOCAL_PATH, '.secret_key_store')) +SECRET_KEY = '<%= @secret_key %>' + +# We recommend you use memcached for development; otherwise after every reload +# of the django development server, you will have to login again. To use +# memcached set CACHES to something like +# CACHES = { +# 'default': { +# 'BACKEND' : 'django.core.cache.backends.memcached.MemcachedCache', +# 'LOCATION' : '127.0.0.1:11211', +# } +#} + +CACHES = { + 'default': { + <% if @cache_server_ip %> +# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + <% if @cache_server_ip.kind_of?(Array) %> + <% split = ":" + @cache_server_port + "','" %> + 'LOCATION': [ <% @cache_server_ip.each do |ip| -%>'<%= ip -%>:<%= @cache_server_port -%>',<% end -%> ], + <% else %> + 'LOCATION': '<%= @cache_server_ip %>:<%= @cache_server_port %>', + <% end %> + <% else %> + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' + <% end %> + } +} + + +# Send email to the console by default +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +# Or send them to /dev/null +#EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + +# Configure these for your outgoing email host +# EMAIL_HOST = 'smtp.my-company.com' +# EMAIL_PORT = 25 +# EMAIL_HOST_USER = 'djangomail' +# EMAIL_HOST_PASSWORD = 'top-secret!' + +# For multiple regions uncomment this configuration, and add (endpoint, title). +<% if @available_regions.kind_of?(Array) %> +AVAILABLE_REGIONS = [ +<% @available_regions.each do |r| -%> + ('<%= r[0] -%>', '<%= r[1] -%>'), +<% end -%> +] +<% end -%> + +<% +if (!@keystone_scheme.nil?) || (!@keystone_host.nil?) || (!@keystone_port.nil?) + @keystone_scheme ||= "http" + @keystone_host ||= "127.0.0.1" + @keystone_port ||= "5000" + @keystone_url = "#{@keystone_scheme}://#{@keystone_host}:#{@keystone_port}/v2.0" +end +-%> +OPENSTACK_KEYSTONE_URL = "<%= @keystone_url %>" +OPENSTACK_KEYSTONE_DEFAULT_ROLE = "<%= @keystone_default_role %>" + +# Disable SSL certificate checks (useful for self-signed certificates): +# OPENSTACK_SSL_NO_VERIFY = True + +# The CA certificate to use to verify SSL connections +# OPENSTACK_SSL_CACERT = '/path/to/cacert.pem' + +# The OPENSTACK_KEYSTONE_BACKEND settings can be used to identify the +# capabilities of the auth backend for Keystone. +# If Keystone has been configured to use LDAP as the auth backend then set +# can_edit_user to False and name to 'ldap'. +# +# TODO(tres): Remove these once Keystone has an API to identify auth backend. +OPENSTACK_KEYSTONE_BACKEND = { + 'name': 'native', + 'can_edit_user': True, + 'can_edit_group': True, + 'can_edit_project': True, + 'can_edit_domain': True, + 'can_edit_role': True +} + +# The OPENSTACK_HYPERVISOR_FEATURES settings can be used to enable optional +# services provided by hypervisors. +OPENSTACK_HYPERVISOR_FEATURES = { + <%- @hypervisor_options = @hypervisor_defaults.merge(@hypervisor_options) -%> + 'can_set_mount_point': <%= @hypervisor_options['can_set_mount_point'].to_s.capitalize %>, + 'can_set_password': <%= @hypervisor_options['can_set_password'].to_s.capitalize %>, +} + +# The OPENSTACK_NEUTRON_NETWORK settings can be used to enable optional +# services provided by neutron. Options currenly available are load +# balancer service, security groups, quotas, VPN service. +OPENSTACK_NEUTRON_NETWORK = { + <%- @neutron_options = @neutron_defaults.merge(@neutron_options) -%> + 'enable_lb': <%= @neutron_options['enable_lb'].to_s.capitalize %>, + 'enable_firewall': <%= @neutron_options['enable_firewall'].to_s.capitalize %>, + 'enable_quotas': <%= @neutron_options['enable_quotas'].to_s.capitalize %>, + 'enable_security_group': <%= @neutron_options['enable_security_group'].to_s.capitalize %>, + 'enable_vpn': <%= @neutron_options['enable_vpn'].to_s.capitalize %>, + # The profile_support option is used to detect if an externa lrouter can be + # configured via the dashboard. When using specific plugins the + # profile_support can be turned on if needed. + <%- if @neutron_options['profile_support'] != 'None' -%> + 'profile_support': '<%= @neutron_options['profile_support'] %>', + <%- end -%> + #'profile_support': 'cisco', +} + +# The OPENSTACK_IMAGE_BACKEND settings can be used to customize features +# in the OpenStack Dashboard related to the Image service, such as the list +# of supported image formats. +# OPENSTACK_IMAGE_BACKEND = { +# 'image_formats': [ +# ('', ''), +# ('aki', _('AKI - Amazon Kernel Image')), +# ('ami', _('AMI - Amazon Machine Image')), +# ('ari', _('ARI - Amazon Ramdisk Image')), +# ('iso', _('ISO - Optical Disk Image')), +# ('qcow2', _('QCOW2 - QEMU Emulator')), +# ('raw', _('Raw')), +# ('vdi', _('VDI')), +# ('vhd', _('VHD')), +# ('vmdk', _('VMDK')) +# ] +# } + +# OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints +# in the Keystone service catalog. Use this setting when Horizon is running +# external to the OpenStack environment. The default is 'publicURL'. +#OPENSTACK_ENDPOINT_TYPE = "publicURL" +<% if @openstack_endpoint_type %> +OPENSTACK_ENDPOINT_TYPE = "<%= @openstack_endpoint_type %>" +<% end %> + +# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the +# case that OPENSTACK_ENDPOINT_TYPE is not present in the endpoints +# in the Keystone service catalog. Use this setting when Horizon is running +# external to the OpenStack environment. The default is None. This +# value should differ from OPENSTACK_ENDPOINT_TYPE if used. +#SECONDARY_ENDPOINT_TYPE = "publicURL" +<% if @secondary_endpoint_type %> +SECONDARY_ENDPOINT_TYPE = "<%= @secondary_endpoint_type %>" +<% end %> + +# The number of objects (Swift containers/objects or images) to display +# on a single page before providing a paging element (a "more" link) +# to paginate results. +API_RESULT_LIMIT = <%= @api_result_limit %> +API_RESULT_PAGE_SIZE = 20 + +# The timezone of the server. This should correspond with the timezone +# of your entire OpenStack installation, and hopefully be in UTC. +TIME_ZONE = "UTC" + +# If you have external monitoring links, eg: +<% if @horizon_app_links %> +EXTERNAL_MONITORING = <%= @horizon_app_links %> +<% end %> + +# When launching an instance, the menu of available flavors is +# sorted by RAM usage, ascending. Provide a callback method here +# (and/or a flag for reverse sort) for the sorted() method if you'd +# like a different behaviour. For more info, see +# http://docs.python.org/2/library/functions.html#sorted +# CREATE_INSTANCE_FLAVOR_SORT = { +# 'key': my_awesome_callback_method, +# 'reverse': False, +# } + +# The Horizon Policy Enforcement engine uses these values to load per service +# policy rule files. The content of these files should match the files the +# OpenStack services are using to determine role based access control in the +# target installation. + +# Path to directory containing policy.json files +#POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf") +<% if @osfamily == 'RedHat' %> +POLICY_FILES_PATH = '/etc/openstack-dashboard' +<% end %> +# Map of local copy of service policy files +#POLICY_FILES = { +# 'identity': 'keystone_policy.json', +# 'compute': 'nova_policy.json' +#} + +# Trove user and database extension support. By default support for +# creating users and databases on database instances is turned on. +# To disable these extensions set the permission here to something +# unusable such as ["!"]. +# TROVE_ADD_USER_PERMS = [] +# TROVE_ADD_DATABASE_PERMS = [] + +LOGGING = { + 'version': 1, + # When set to True this will disable all logging except + # for loggers specified in this configuration dictionary. Note that + # if nothing is specified here and disable_existing_loggers is True, + # django.db.backends will still log unless it is disabled explicitly. + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(asctime)s %(process)d %(levelname)s %(name)s ' + '%(message)s' + }, + }, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'django.utils.log.NullHandler', + }, + 'console': { + # Set the level to "DEBUG" for verbose output logging. + 'level': 'INFO', + 'class': 'logging.StreamHandler', + }, + 'file': { + 'level': '<%= @log_level %>', + 'class': 'logging.FileHandler', + 'filename': '<%= scope.lookupvar("horizon::params::logdir") %>/horizon.log', + 'formatter': 'verbose', + }, + }, + 'loggers': { + # Logging from django.db.backends is VERY verbose, send to null + # by default. + 'django.db.backends': { + 'handlers': ['null'], + 'propagate': False, + }, + 'requests': { + 'handlers': ['null'], + 'propagate': False, + }, + 'horizon': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'openstack_dashboard': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'novaclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'cinderclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'keystoneclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'glanceclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'neutronclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'heatclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'ceilometerclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'troveclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'swiftclient': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'openstack_auth': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'nose.plugins.manager': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + 'django': { + # 'handlers': ['console'], + 'handlers': ['file'], + # 'level': 'DEBUG', + 'level': '<%= @log_level %>', + 'propagate': False, + }, + } +} + +SECURITY_GROUP_RULES = { + 'all_tcp': { + 'name': 'ALL TCP', + 'ip_protocol': 'tcp', + 'from_port': '1', + 'to_port': '65535', + }, + 'all_udp': { + 'name': 'ALL UDP', + 'ip_protocol': 'udp', + 'from_port': '1', + 'to_port': '65535', + }, + 'all_icmp': { + 'name': 'ALL ICMP', + 'ip_protocol': 'icmp', + 'from_port': '-1', + 'to_port': '-1', + }, + 'ssh': { + 'name': 'SSH', + 'ip_protocol': 'tcp', + 'from_port': '22', + 'to_port': '22', + }, + 'smtp': { + 'name': 'SMTP', + 'ip_protocol': 'tcp', + 'from_port': '25', + 'to_port': '25', + }, + 'dns': { + 'name': 'DNS', + 'ip_protocol': 'tcp', + 'from_port': '53', + 'to_port': '53', + }, + 'http': { + 'name': 'HTTP', + 'ip_protocol': 'tcp', + 'from_port': '80', + 'to_port': '80', + }, + 'pop3': { + 'name': 'POP3', + 'ip_protocol': 'tcp', + 'from_port': '110', + 'to_port': '110', + }, + 'imap': { + 'name': 'IMAP', + 'ip_protocol': 'tcp', + 'from_port': '143', + 'to_port': '143', + }, + 'ldap': { + 'name': 'LDAP', + 'ip_protocol': 'tcp', + 'from_port': '389', + 'to_port': '389', + }, + 'https': { + 'name': 'HTTPS', + 'ip_protocol': 'tcp', + 'from_port': '443', + 'to_port': '443', + }, + 'smtps': { + 'name': 'SMTPS', + 'ip_protocol': 'tcp', + 'from_port': '465', + 'to_port': '465', + }, + 'imaps': { + 'name': 'IMAPS', + 'ip_protocol': 'tcp', + 'from_port': '993', + 'to_port': '993', + }, + 'pop3s': { + 'name': 'POP3S', + 'ip_protocol': 'tcp', + 'from_port': '995', + 'to_port': '995', + }, + 'ms_sql': { + 'name': 'MS SQL', + 'ip_protocol': 'tcp', + 'from_port': '1433', + 'to_port': '1433', + }, + 'mysql': { + 'name': 'MYSQL', + 'ip_protocol': 'tcp', + 'from_port': '3306', + 'to_port': '3306', + }, + 'rdp': { + 'name': 'RDP', + 'ip_protocol': 'tcp', + 'from_port': '3389', + 'to_port': '3389', + }, +} + +LOGIN_URL = '<%= scope.lookupvar("horizon::params::root_url") %>/auth/login/' +LOGOUT_URL = '<%= scope.lookupvar("horizon::params::root_url") %>/auth/logout/' +LOGIN_REDIRECT_URL = '<%= scope.lookupvar("horizon::params::root_url") %>' + +# The Ubuntu package includes pre-compressed JS and compiled CSS to allow +# offline compression by default. To enable online compression, install +# the python-lesscpy package and disable the following option. +COMPRESS_OFFLINE = <%= @compress_offline.to_s.capitalize %> + +# For Glance image upload, Horizon uses the file upload support from Django +# so we add this option to change the directory where uploaded files are temporarily +# stored until they are loaded into Glance. +FILE_UPLOAD_TEMP_DIR = '<%= @file_upload_temp_dir %>' diff --git a/inifile b/inifile deleted file mode 160000 index fe9b0d522..000000000 --- a/inifile +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fe9b0d5229ea37179a08c4b49239da9bc950acd1 diff --git a/inifile/.fixtures.yml b/inifile/.fixtures.yml new file mode 100644 index 000000000..f74b0177c --- /dev/null +++ b/inifile/.fixtures.yml @@ -0,0 +1,3 @@ +fixtures: + symlinks: + inifile: '#{source_dir}' diff --git a/inifile/.gitignore b/inifile/.gitignore new file mode 100644 index 000000000..f9d3de20b --- /dev/null +++ b/inifile/.gitignore @@ -0,0 +1,4 @@ +spec/fixtures/modules/inifile +spec/fixtures/manifests/site.pp +spec/fixtures/tmp/* +Gemfile.lock diff --git a/inifile/.travis.yml b/inifile/.travis.yml new file mode 100644 index 000000000..eae801faa --- /dev/null +++ b/inifile/.travis.yml @@ -0,0 +1,40 @@ +--- +branches: + only: + - master +language: ruby +bundler_args: --without development +script: bundle exec rake spec SPEC_OPTS='--format documentation' +after_success: + - git clone -q git://github.com/puppetlabs/ghpublisher.git .forge-releng + - .forge-releng/publish +rvm: + - 1.8.7 + - 1.9.3 + - 2.0.0 +env: + matrix: + - PUPPET_GEM_VERSION="~> 2.7.0" + - PUPPET_GEM_VERSION="~> 3.0.0" + - PUPPET_GEM_VERSION="~> 3.1.0" + - PUPPET_GEM_VERSION="~> 3.2.0" + global: + - PUBLISHER_LOGIN=puppetlabs + - secure: |- + mEqZt84gUpTwozeWT4ra2NIQID4CHkvcDcL/8+ItnS3ahbZ4EOj4NLnEWcNt\nxtOjs6pxn + GVND1/gy/1+7w81DpXpJQ6CGloEwP58x86icvrgp8E+ZXeAoYe4\nfqtSE190FMq5FWaPyx + wZisaT2OcrpeqvBPQnaGUg4uz/H3djgUo= +matrix: + exclude: + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 2.7.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 2.7.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 3.0.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 3.1.0" + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 3.2.0" +notifications: + email: false diff --git a/inifile/CHANGELOG b/inifile/CHANGELOG new file mode 100644 index 000000000..54a245f98 --- /dev/null +++ b/inifile/CHANGELOG @@ -0,0 +1,89 @@ +2013-07-16 - Version 1.0.0 +Features: +- Handle empty values. +- Handle whitespace in settings names (aka: server role = something) +- Add mechanism for allowing ini_setting subclasses to override the +formation of the namevar during .instances, to allow for ini_setting +derived types that manage flat ini-file-like files and still purge +them. + +2013-05-28 - Chris Price - 0.10.3 + * Fix bug in subsetting handling for new settings (cbea5dc) + +2013-05-22 - Chris Price - 0.10.2 + * Better handling of quotes for subsettings (1aa7e60) + +2013-05-21 - Chris Price - 0.10.1 + * Change constants to class variables to avoid ruby warnings (6b19864) + +2013-04-10 - Erik Dalén - 0.10.1 + * Style fixes (c4af8c3) + +2013-04-02 - Dan Bode - 0.10.1 + * Add travisfile and Gemfile (c2052b3) + +2013-04-02 - Chris Price - 0.10.1 + * Update README.markdown (ad38a08) + +2013-02-15 - Karel Brezina - 0.10.0 + * Added 'ini_subsetting' custom resource type (4351d8b) + +2013-03-11 - Dan Bode - 0.10.0 + * guard against nil indentation values (5f71d7f) + +2013-01-07 - Dan Bode - 0.10.0 + * Add purging support to ini file (2f22483) + +2013-02-05 - James Sweeny - 0.10.0 + * Fix test to use correct key_val_parameter (b1aff63) + +2012-11-06 - Chris Price - 0.10.0 + * Added license file w/Apache 2.0 license (5e1d203) + +2012-11-02 - Chris Price - 0.9.0 + * Version 0.9.0 released + +2012-10-26 - Chris Price - 0.9.0 + * Add detection for commented versions of settings (a45ab65) + +2012-10-20 - Chris Price - 0.9.0 + * Refactor to clarify implementation of `save` (f0d443f) + +2012-10-20 - Chris Price - 0.9.0 + * Add example for `ensure=absent` (e517148) + +2012-10-20 - Chris Price - 0.9.0 + * Better handling of whitespace lines at ends of sections (845fa70) + +2012-10-20 - Chris Price - 0.9.0 + * Respect indentation / spacing for existing sections and settings (c2c26de) + +2012-10-17 - Chris Price - 0.9.0 + * Minor tweaks to handling of removing settings (cda30a6) + +2012-10-10 - Dan Bode - 0.9.0 + * Add support for removing lines (1106d70) + +2012-10-02 - Dan Bode - 0.9.0 + * Make value a property (cbc90d3) + +2012-10-02 - Dan Bode - 0.9.0 + * Make ruby provider a better parent. (1564c47) + +2012-09-29 - Reid Vandewiele - 0.9.0 + * Allow values with spaces to be parsed and set (3829e20) + +2012-09-24 - Chris Price - 0.0.3 + * Version 0.0.3 released + +2012-09-20 - Chris Price - 0.0.3 + * Add validation for key_val_separator (e527908) + +2012-09-19 - Chris Price - 0.0.3 + * Allow overriding separator string between key/val pairs (8d1fdc5) + +2012-08-20 - Chris Price - 0.0.2 + * Version 0.0.2 released + +2012-08-17 - Chris Price - 0.0.2 + * Add support for "global" section at beginning of file (c57dab4) diff --git a/inifile/Gemfile b/inifile/Gemfile new file mode 100644 index 000000000..72e8342df --- /dev/null +++ b/inifile/Gemfile @@ -0,0 +1,17 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'rake', :require => false + gem 'rspec-puppet', :require => false + gem 'puppetlabs_spec_helper', :require => false + gem 'simplecov', :require => false + gem 'pry', :require => false +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/inifile/LICENSE b/inifile/LICENSE new file mode 100644 index 000000000..73caedae9 --- /dev/null +++ b/inifile/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 2012 Chris Price + + 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/inifile/Modulefile b/inifile/Modulefile new file mode 100644 index 000000000..963f950c1 --- /dev/null +++ b/inifile/Modulefile @@ -0,0 +1,8 @@ +name 'puppetlabs-inifile' +version '1.0.0' +source 'git://github.com/puppetlabs/puppetlabs-inifile.git' +author 'Puppetlabs' +description 'Resource types for managing settings in INI files' +summary 'Resource types for managing settings in INI files' +license 'Apache' +project_page 'https://github.com/puppetlabs/puppetlabs-inifile' diff --git a/inifile/README.markdown b/inifile/README.markdown new file mode 100644 index 000000000..a2e1652ab --- /dev/null +++ b/inifile/README.markdown @@ -0,0 +1,107 @@ +[![Build Status](https://travis-ci.org/puppetlabs/puppetlabs-inifile.png?branch=master)](https://travis-ci.org/cprice-puppet/puppetlabs-inifile) + +# INI-file module # + +This module provides resource types for use in managing INI-style configuration +files. The main resource type is `ini_setting`, which is used to manage an +individual setting in an INI file. Here's an example usage: + + ini_setting { "sample setting": + path => '/tmp/foo.ini', + section => 'foo', + setting => 'foosetting', + value => 'FOO!', + ensure => present, + } + +A supplementary resource type is `ini_subsetting`, which is used to manage +settings that consist of several arguments such as + + JAVA_ARGS="-Xmx192m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/pe-puppetdb/puppetdb-oom.hprof " + + ini_subsetting {'sample subsetting': + ensure => present, + section => '', + key_val_separator => '=', + path => '/etc/default/pe-puppetdb', + setting => 'JAVA_ARGS', + subsetting => '-Xmx', + value => '512m', + } + +## implementing child providers: + + +The ini_setting class can be overridden by child providers in order to implement the management of ini settings for a specific configuration file. + +In order to implement this, you will need to specify your own Type (as shown below). This type needs to implement a namevar (name), and a property called value: + + + example: + + #my_module/lib/puppet/type/glance_api_config.rb + Puppet::Type.newtype(:glance_api_config) do + ensurable + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from glance-api.conf' + # namevar should be of the form section/setting + newvalues(/\S+\/\S+/) + end + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |v| + v.to_s.strip + end + end + end + +This type also must have a provider that utilizes the ini_setting provider as its parent: + + example: + + # my_module/lib/puppet/provider/glance_api_config/ini_setting.rb + Puppet::Type.type(:glance_api_config).provide( + :ini_setting, + # set ini_setting as the parent provider + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) + ) do + # implement section as the first part of the namevar + def section + resource[:name].split('/', 2).first + end + def setting + # implement setting as the second part of the namevar + resource[:name].split('/', 2).last + end + # hard code the file path (this allows purging) + def self.file_path + '/etc/glance/glance-api.conf' + end + end + + +Now, the individual settings of the /etc/glance/glance-api.conf file can be managed as individual resources: + + glance_api_config { 'HEADER/important_config': + value => 'secret_value', + } + +Provided that self.file_path has been implemented, you can purge with the following puppet syntax: + + resources { 'glance_api_config' + purge => true, + } + +If the above code is added, then the resulting configured file will only contain lines implemented as Puppet resources + + +## A few noteworthy features: + + * The module tries *hard* not to manipulate your file any more than it needs to. + In most cases, it should leave the original whitespace, comments, ordering, + etc. perfectly intact. + * Supports comments starting with either '#' or ';'. + * Will add missing sections if they don't exist. + * Supports a "global" section (settings that go at the beginning of the file, + before any named sections) by specifying a section name of "". + diff --git a/inifile/Rakefile b/inifile/Rakefile new file mode 100644 index 000000000..cd3d37995 --- /dev/null +++ b/inifile/Rakefile @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/rake_tasks' diff --git a/inifile/lib/puppet/provider/ini_setting/ruby.rb b/inifile/lib/puppet/provider/ini_setting/ruby.rb new file mode 100644 index 000000000..1b2bc80ee --- /dev/null +++ b/inifile/lib/puppet/provider/ini_setting/ruby.rb @@ -0,0 +1,102 @@ +require File.expand_path('../../../util/ini_file', __FILE__) + +Puppet::Type.type(:ini_setting).provide(:ruby) do + + def self.instances + # this code is here to support purging and the query-all functionality of the + # 'puppet resource' command, on a per-file basis. Users + # can create a type for a specific config file with a provider that uses + # this as its parent and implements the method + # 'self.file_path', and that will provide the value for the path to the + # ini file (rather than needing to specify it on each ini setting + # declaration). This allows 'purging' to be used to clear out + # all settings from a particular ini file except those included in + # the catalog. + if self.respond_to?(:file_path) + # figure out what to do about the seperator + ini_file = Puppet::Util::IniFile.new(file_path, '=') + resources = [] + ini_file.section_names.each do |section_name| + ini_file.get_settings(section_name).each do |setting, value| + resources.push( + new( + :name => namevar(section_name, setting), + :value => value, + :ensure => :present + ) + ) + end + end + resources + else + raise(Puppet::Error, 'Ini_settings only support collecting instances when a file path is hard coded') + end + end + + def self.namevar(section_name, setting) + "#{section_name}/#{setting}" + end + + def exists? + ini_file.get_value(section, setting) + end + + def create + ini_file.set_value(section, setting, resource[:value]) + ini_file.save + @ini_file = nil + end + + def destroy + ini_file.remove_setting(section, setting) + ini_file.save + @ini_file = nil + end + + def value + ini_file.get_value(section, setting) + end + + def value=(value) + ini_file.set_value(section, setting, resource[:value]) + ini_file.save + end + + def section + # this method is here so that it can be overridden by a child provider + resource[:section] + end + + def setting + # this method is here so that it can be overridden by a child provider + resource[:setting] + end + + def file_path + # this method is here to support purging and sub-classing. + # if a user creates a type and subclasses our provider and provides a + # 'file_path' method, then they don't have to specify the + # path as a parameter for every ini_setting declaration. + # This implementation allows us to support that while still + # falling back to the parameter value when necessary. + if self.class.respond_to?(:file_path) + self.class.file_path + else + resource[:path] + end + end + + def separator + if resource.class.validattr?(:key_val_separator) + resource[:key_val_separator] || '=' + else + '=' + end + end + + private + def ini_file + @ini_file ||= Puppet::Util::IniFile.new(file_path, separator) + end + +end diff --git a/inifile/lib/puppet/provider/ini_subsetting/ruby.rb b/inifile/lib/puppet/provider/ini_subsetting/ruby.rb new file mode 100644 index 000000000..49c0e49ba --- /dev/null +++ b/inifile/lib/puppet/provider/ini_subsetting/ruby.rb @@ -0,0 +1,70 @@ +require File.expand_path('../../../util/ini_file', __FILE__) +require File.expand_path('../../../util/setting_value', __FILE__) + +Puppet::Type.type(:ini_subsetting).provide(:ruby) do + + def exists? + setting_value.get_subsetting_value(subsetting) + end + + def create + setting_value.add_subsetting(subsetting, resource[:value]) + ini_file.set_value(section, setting, setting_value.get_value) + ini_file.save + @ini_file = nil + @setting_value = nil + end + + def destroy + setting_value.remove_subsetting(subsetting) + ini_file.set_value(section, setting, setting_value.get_value) + ini_file.save + @ini_file = nil + @setting_value = nil + end + + def value + setting_value.get_subsetting_value(subsetting) + end + + def value=(value) + setting_value.add_subsetting(subsetting, resource[:value]) + ini_file.set_value(section, setting, setting_value.get_value) + ini_file.save + end + + def section + resource[:section] + end + + def setting + resource[:setting] + end + + def subsetting + resource[:subsetting] + end + + def subsetting_separator + resource[:subsetting_separator] + end + + def file_path + resource[:path] + end + + def separator + resource[:key_val_separator] || '=' + end + + private + def ini_file + @ini_file ||= Puppet::Util::IniFile.new(file_path, separator) + end + + private + def setting_value + @setting_value ||= Puppet::Util::SettingValue.new(ini_file.get_value(section, setting), subsetting_separator) + end + +end diff --git a/inifile/lib/puppet/type/ini_setting.rb b/inifile/lib/puppet/type/ini_setting.rb new file mode 100644 index 000000000..450623143 --- /dev/null +++ b/inifile/lib/puppet/type/ini_setting.rb @@ -0,0 +1,47 @@ +Puppet::Type.newtype(:ini_setting) do + + ensurable do + defaultvalues + defaultto :present + end + + newparam(:name, :namevar => true) do + desc 'An arbitrary name used as the identity of the resource.' + end + + newparam(:section) do + desc 'The name of the section in the ini file in which the setting should be defined.' + end + + newparam(:setting) do + desc 'The name of the setting to be defined.' + end + + newparam(:path) do + desc 'The ini file Puppet will ensure contains the specified setting.' + validate do |value| + unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) + raise(Puppet::Error, "File paths must be fully qualified, not '#{value}'") + end + end + end + + newparam(:key_val_separator) do + desc 'The separator string to use between each setting name and value. ' + + 'Defaults to " = ", but you could use this to override e.g. whether ' + + 'or not the separator should include whitespace.' + defaultto(" = ") + + validate do |value| + unless value.scan('=').size == 1 + raise Puppet::Error, ":key_val_separator must contain exactly one = character." + end + end + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + end + + +end diff --git a/inifile/lib/puppet/type/ini_subsetting.rb b/inifile/lib/puppet/type/ini_subsetting.rb new file mode 100644 index 000000000..dd146c291 --- /dev/null +++ b/inifile/lib/puppet/type/ini_subsetting.rb @@ -0,0 +1,55 @@ +Puppet::Type.newtype(:ini_subsetting) do + + ensurable do + defaultvalues + defaultto :present + end + + newparam(:name, :namevar => true) do + desc 'An arbitrary name used as the identity of the resource.' + end + + newparam(:section) do + desc 'The name of the section in the ini file in which the setting should be defined.' + end + + newparam(:setting) do + desc 'The name of the setting to be defined.' + end + + newparam(:subsetting) do + desc 'The name of the subsetting to be defined.' + end + + newparam(:subsetting_separator) do + desc 'The separator string between subsettings. Defaults to " "' + defaultto(" ") + end + + newparam(:path) do + desc 'The ini file Puppet will ensure contains the specified setting.' + validate do |value| + unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) + raise(Puppet::Error, "File paths must be fully qualified, not '#{value}'") + end + end + end + + newparam(:key_val_separator) do + desc 'The separator string to use between each setting name and value. ' + + 'Defaults to " = ", but you could use this to override e.g. whether ' + + 'or not the separator should include whitespace.' + defaultto(" = ") + + validate do |value| + unless value.scan('=').size == 1 + raise Puppet::Error, ":key_val_separator must contain exactly one = character." + end + end + end + + newproperty(:value) do + desc 'The value of the subsetting to be defined.' + end + +end diff --git a/inifile/lib/puppet/util/external_iterator.rb b/inifile/lib/puppet/util/external_iterator.rb new file mode 100644 index 000000000..45c0fa434 --- /dev/null +++ b/inifile/lib/puppet/util/external_iterator.rb @@ -0,0 +1,28 @@ +module Puppet +module Util + class ExternalIterator + def initialize(coll) + @coll = coll + @cur_index = -1 + end + + def next + @cur_index = @cur_index + 1 + item_at(@cur_index) + end + + def peek + item_at(@cur_index + 1) + end + + private + def item_at(index) + if @coll.length > index + [@coll[index], index] + else + [nil, nil] + end + end + end +end +end diff --git a/inifile/lib/puppet/util/ini_file.rb b/inifile/lib/puppet/util/ini_file.rb new file mode 100644 index 000000000..53bed2d5f --- /dev/null +++ b/inifile/lib/puppet/util/ini_file.rb @@ -0,0 +1,300 @@ +require File.expand_path('../external_iterator', __FILE__) +require File.expand_path('../ini_file/section', __FILE__) + +module Puppet +module Util + class IniFile + + @@SECTION_REGEX = /^\s*\[([\w\d\.\\\/\-\:]+)\]\s*$/ + @@SETTING_REGEX = /^(\s*)([\w\d\.\\\/\-\s]*[\w\d\.\\\/\-])([ \t]*=[ \t]*)([\S\s]*?)\s*$/ + @@COMMENTED_SETTING_REGEX = /^(\s*)[#;]+(\s*)([\w\d\.\\\/\-]+)([ \t]*=[ \t]*)([\S\s]*?)\s*$/ + + def initialize(path, key_val_separator = ' = ') + @path = path + @key_val_separator = key_val_separator + @section_names = [] + @sections_hash = {} + if File.file?(@path) + parse_file + end + end + + def section_names + @section_names + end + + def get_settings(section_name) + section = @sections_hash[section_name] + section.setting_names.inject({}) do |result, setting| + result[setting] = section.get_value(setting) + result + end + end + + def get_value(section_name, setting) + if (@sections_hash.has_key?(section_name)) + @sections_hash[section_name].get_value(setting) + end + end + + def set_value(section_name, setting, value) + unless (@sections_hash.has_key?(section_name)) + add_section(Section.new(section_name, nil, nil, nil, nil)) + end + + section = @sections_hash[section_name] + + if (section.has_existing_setting?(setting)) + update_line(section, setting, value) + section.update_existing_setting(setting, value) + elsif result = find_commented_setting(section, setting) + # So, this stanza is a bit of a hack. What we're trying + # to do here is this: for settings that don't already + # exist, we want to take a quick peek to see if there + # is a commented-out version of them in the section. + # If so, we'd prefer to add the setting directly after + # the commented line, rather than at the end of the section. + + # If we get here then we found a commented line, so we + # call "insert_inline_setting_line" to update the lines array + insert_inline_setting_line(result, section, setting, value) + + # Then, we need to tell the setting object that we hacked + # in an inline setting + section.insert_inline_setting(setting, value) + + # Finally, we need to update all of the start/end line + # numbers for all of the sections *after* the one that + # was modified. + section_index = @section_names.index(section_name) + increment_section_line_numbers(section_index + 1) + else + section.set_additional_setting(setting, value) + end + end + + def remove_setting(section_name, setting) + section = @sections_hash[section_name] + if (section.has_existing_setting?(setting)) + # If the setting is found, we have some work to do. + # First, we remove the line from our array of lines: + remove_line(section, setting) + + # Then, we need to tell the setting object to remove + # the setting from its state: + section.remove_existing_setting(setting) + + # Finally, we need to update all of the start/end line + # numbers for all of the sections *after* the one that + # was modified. + section_index = @section_names.index(section_name) + decrement_section_line_numbers(section_index + 1) + end + end + + def save + File.open(@path, 'w') do |fh| + + @section_names.each_index do |index| + name = @section_names[index] + + section = @sections_hash[name] + + # We need a buffer to cache lines that are only whitespace + whitespace_buffer = [] + + if (section.is_new_section?) && (! section.is_global?) + fh.puts("\n[#{section.name}]") + end + + if ! section.is_new_section? + # write all of the pre-existing settings + (section.start_line..section.end_line).each do |line_num| + line = lines[line_num] + + # We buffer any lines that are only whitespace so that + # if they are at the end of a section, we can insert + # any new settings *before* the final chunk of whitespace + # lines. + if (line =~ /^\s*$/) + whitespace_buffer << line + else + # If we get here, we've found a non-whitespace line. + # We'll flush any cached whitespace lines before we + # write it. + flush_buffer_to_file(whitespace_buffer, fh) + fh.puts(line) + end + end + end + + # write new settings, if there are any + section.additional_settings.each_pair do |key, value| + fh.puts("#{' ' * (section.indentation || 0)}#{key}#{@key_val_separator}#{value}") + end + + if (whitespace_buffer.length > 0) + flush_buffer_to_file(whitespace_buffer, fh) + else + # We get here if there were no blank lines at the end of the + # section. + # + # If we are adding a new section with a new setting, + # and if there are more sections that come after this one, + # we'll write one blank line just so that there is a little + # whitespace between the sections. + #if (section.end_line.nil? && + if (section.is_new_section? && + (section.additional_settings.length > 0) && + (index < @section_names.length - 1)) + fh.puts("") + end + end + + end + end + end + + + private + def add_section(section) + @sections_hash[section.name] = section + @section_names << section.name + end + + def parse_file + line_iter = create_line_iter + + # We always create a "global" section at the beginning of the file, for + # anything that appears before the first named section. + section = read_section('', 0, line_iter) + add_section(section) + line, line_num = line_iter.next + + while line + if (match = @@SECTION_REGEX.match(line)) + section = read_section(match[1], line_num, line_iter) + add_section(section) + end + line, line_num = line_iter.next + end + end + + def read_section(name, start_line, line_iter) + settings = {} + end_line_num = nil + min_indentation = nil + while true + line, line_num = line_iter.peek + if (line_num.nil? or match = @@SECTION_REGEX.match(line)) + return Section.new(name, start_line, end_line_num, settings, min_indentation) + elsif (match = @@SETTING_REGEX.match(line)) + settings[match[2]] = match[4] + indentation = match[1].length + min_indentation = [indentation, min_indentation || indentation].min + end + end_line_num = line_num + line_iter.next + end + end + + def update_line(section, setting, value) + (section.start_line..section.end_line).each do |line_num| + if (match = @@SETTING_REGEX.match(lines[line_num])) + if (match[2] == setting) + lines[line_num] = "#{match[1]}#{match[2]}#{match[3]}#{value}" + end + end + end + end + + def remove_line(section, setting) + (section.start_line..section.end_line).each do |line_num| + if (match = @@SETTING_REGEX.match(lines[line_num])) + if (match[2] == setting) + lines.delete_at(line_num) + end + end + end + end + + def create_line_iter + ExternalIterator.new(lines) + end + + def lines + @lines ||= IniFile.readlines(@path) + end + + # This is mostly here because it makes testing easier--we don't have + # to try to stub any methods on File. + def self.readlines(path) + # If this type is ever used with very large files, we should + # write this in a different way, using a temp + # file; for now assuming that this type is only used on + # small-ish config files that can fit into memory without + # too much trouble. + File.readlines(path) + end + + # This utility method scans through the lines for a section looking for + # commented-out versions of a setting. It returns `nil` if it doesn't + # find one. If it does find one, then it returns a hash containing + # two keys: + # + # :line_num - the line number that contains the commented version + # of the setting + # :match - the ruby regular expression match object, which can + # be used to mimic the whitespace from the comment line + def find_commented_setting(section, setting) + return nil if section.is_new_section? + (section.start_line..section.end_line).each do |line_num| + if (match = @@COMMENTED_SETTING_REGEX.match(lines[line_num])) + if (match[3] == setting) + return { :match => match, :line_num => line_num } + end + end + end + nil + end + + # This utility method is for inserting a line into the existing + # lines array. The `result` argument is expected to be in the + # format of the return value of `find_commented_setting`. + def insert_inline_setting_line(result, section, setting, value) + line_num = result[:line_num] + match = result[:match] + lines.insert(line_num + 1, "#{' ' * (section.indentation || 0 )}#{setting}#{match[4]}#{value}") + end + + # Utility method; given a section index (index into the @section_names + # array), decrement the start/end line numbers for that section and all + # all of the other sections that appear *after* the specified section. + def decrement_section_line_numbers(section_index) + @section_names[section_index..(@section_names.length - 1)].each do |name| + section = @sections_hash[name] + section.decrement_line_nums + end + end + + # Utility method; given a section index (index into the @section_names + # array), increment the start/end line numbers for that section and all + # all of the other sections that appear *after* the specified section. + def increment_section_line_numbers(section_index) + @section_names[section_index..(@section_names.length - 1)].each do |name| + section = @sections_hash[name] + section.increment_line_nums + end + end + + + def flush_buffer_to_file(buffer, fh) + if buffer.length > 0 + buffer.each { |l| fh.puts(l) } + buffer.clear + end + end + + end +end +end diff --git a/inifile/lib/puppet/util/ini_file/section.rb b/inifile/lib/puppet/util/ini_file/section.rb new file mode 100644 index 000000000..9682d7ff9 --- /dev/null +++ b/inifile/lib/puppet/util/ini_file/section.rb @@ -0,0 +1,103 @@ +module Puppet +module Util +class IniFile + class Section + # Some implementation details: + # + # * `name` will be set to the empty string for the 'global' section. + # * there will always be a 'global' section, with a `start_line` of 0, + # but if the file actually begins with a real section header on + # the first line, then the 'global' section will have an + # `end_line` of `nil`. + # * `start_line` and `end_line` will be set to `nil` for a new non-global + # section. + def initialize(name, start_line, end_line, settings, indentation) + @name = name + @start_line = start_line + @end_line = end_line + @existing_settings = settings.nil? ? {} : settings + @additional_settings = {} + @indentation = indentation + end + + attr_reader :name, :start_line, :end_line, :additional_settings, :indentation + + def is_global?() + @name == '' + end + + def is_new_section?() + # a new section (global or named) will always have `end_line` + # set to `nil` + @end_line.nil? + end + + def setting_names + @existing_settings.keys | @additional_settings.keys + end + + def get_value(setting_name) + @existing_settings[setting_name] || @additional_settings[setting_name] + end + + def has_existing_setting?(setting_name) + @existing_settings.has_key?(setting_name) + end + + def update_existing_setting(setting_name, value) + @existing_settings[setting_name] = value + end + + def remove_existing_setting(setting_name) + if (@existing_settings.delete(setting_name)) + if @end_line + @end_line = @end_line - 1 + end + end + end + + # This is a hacky method; it's basically called when we need to insert + # a new setting but we don't want it to appear at the very end of the + # section. Instead we hack it into the existing settings list and + # increment our end_line number--this assumes that the caller (`ini_file`) + # is doing some babysitting w/rt the other sections and the actual data + # of the lines. + def insert_inline_setting(setting_name, value) + @existing_settings[setting_name] = value + if @end_line + @end_line = @end_line + 1 + end + end + + def set_additional_setting(setting_name, value) + @additional_settings[setting_name] = value + end + + # Decrement the start and end line numbers for the section (if they are + # defined); this is intended to be called when a setting is removed + # from a section that comes before this section in the ini file. + def decrement_line_nums() + if @start_line + @start_line = @start_line - 1 + end + if @end_line + @end_line = @end_line - 1 + end + end + + # Increment the start and end line numbers for the section (if they are + # defined); this is intended to be called when an inline setting is added + # to a section that comes before this section in the ini file. + def increment_line_nums() + if @start_line + @start_line = @start_line + 1 + end + if @end_line + @end_line = @end_line + 1 + end + end + + end +end +end +end diff --git a/inifile/lib/puppet/util/setting_value.rb b/inifile/lib/puppet/util/setting_value.rb new file mode 100644 index 000000000..42cd28ec6 --- /dev/null +++ b/inifile/lib/puppet/util/setting_value.rb @@ -0,0 +1,93 @@ +module Puppet +module Util + + class SettingValue + + def initialize(setting_value, subsetting_separator = ' ') + @setting_value = setting_value + @subsetting_separator = subsetting_separator + + @quote_char = "" + + if @setting_value + unquoted, @quote_char = unquote_setting_value(setting_value) + @subsetting_items = unquoted.scan(Regexp.new("(?:(?:[^\\#{@subsetting_separator}]|\\.)+)")) # an item can contain escaped separator + @subsetting_items.map! { |item| item.strip } + else + @subsetting_items = [] + end + end + + def unquote_setting_value(setting_value) + quote_char = "" + if (setting_value.start_with?('"') and setting_value.end_with?('"')) + quote_char = '"' + elsif (setting_value.start_with?("'") and setting_value.end_with?("'")) + quote_char = "'" + end + + unquoted = setting_value + + if (quote_char != "") + unquoted = setting_value[1, setting_value.length - 2] + end + + [unquoted, quote_char] + end + + def get_value + + result = "" + first = true + + @subsetting_items.each { |item| + result << @subsetting_separator unless first + result << item + first = false + } + + @quote_char + result + @quote_char + end + + def get_subsetting_value(subsetting) + + value = nil + + @subsetting_items.each { |item| + if(item.start_with?(subsetting)) + value = item[subsetting.length, item.length - subsetting.length] + break + end + } + + value + end + + def add_subsetting(subsetting, subsetting_value) + + new_item = subsetting + (subsetting_value || '') + found = false + + @subsetting_items.map! { |item| + if item.start_with?(subsetting) + value = new_item + found = true + else + value = item + end + + value + } + + unless found + @subsetting_items.push(new_item) + end + end + + def remove_subsetting(subsetting) + @subsetting_items = @subsetting_items.map { |item| item.start_with?(subsetting) ? nil : item }.compact + end + + end +end +end diff --git a/inifile/spec/classes/inherit_test1_spec.rb b/inifile/spec/classes/inherit_test1_spec.rb new file mode 100644 index 000000000..51fc0c22f --- /dev/null +++ b/inifile/spec/classes/inherit_test1_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' +# We can't really test much here, apart from the type roundtrips though the +# parser OK. +describe 'inherit_test1' do + it do + should contain_inherit_ini_setting('valid_type').with({ + 'value' => 'true' + }) + end +end diff --git a/inifile/spec/fixtures/modules/inherit_ini_setting/lib/puppet/provider/inherit_ini_setting/ini_setting.rb b/inifile/spec/fixtures/modules/inherit_ini_setting/lib/puppet/provider/inherit_ini_setting/ini_setting.rb new file mode 100644 index 000000000..f5e461a20 --- /dev/null +++ b/inifile/spec/fixtures/modules/inherit_ini_setting/lib/puppet/provider/inherit_ini_setting/ini_setting.rb @@ -0,0 +1,17 @@ +Puppet::Type.type(:inherit_ini_setting).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + def section + '' # all global + end + + # This type has no sections + def self.namevar(section_name, setting) + setting + end + + def self.file_path + File.expand_path(File.dirname(__FILE__) + '/../../../../../../tmp/inherit_inifile.cfg') + end +end diff --git a/inifile/spec/fixtures/modules/inherit_ini_setting/lib/puppet/type/inherit_ini_setting.rb b/inifile/spec/fixtures/modules/inherit_ini_setting/lib/puppet/type/inherit_ini_setting.rb new file mode 100644 index 000000000..fd939b8ac --- /dev/null +++ b/inifile/spec/fixtures/modules/inherit_ini_setting/lib/puppet/type/inherit_ini_setting.rb @@ -0,0 +1,5 @@ +Puppet::Type.newtype(:inherit_ini_setting) do + ensurable + newparam(:setting, :namevar => true) + newproperty(:value) +end diff --git a/inifile/spec/fixtures/modules/inherit_test1/manifests/init.pp b/inifile/spec/fixtures/modules/inherit_test1/manifests/init.pp new file mode 100644 index 000000000..8f4366077 --- /dev/null +++ b/inifile/spec/fixtures/modules/inherit_test1/manifests/init.pp @@ -0,0 +1,5 @@ +class inherit_test1 { + inherit_ini_setting { 'valid_type': + value => true, + } +} diff --git a/inifile/spec/fixtures/tmp/.empty b/inifile/spec/fixtures/tmp/.empty new file mode 100644 index 000000000..e69de29bb diff --git a/inifile/spec/spec_helper.rb b/inifile/spec/spec_helper.rb new file mode 100644 index 000000000..c2d12b5ec --- /dev/null +++ b/inifile/spec/spec_helper.rb @@ -0,0 +1,13 @@ +gem 'rspec', '>=2.0.0' +require 'rspec/expectations' + + +require 'puppetlabs_spec_helper/puppetlabs_spec_helper' + +require 'puppetlabs_spec_helper/puppetlabs_spec/files' + +require 'puppetlabs_spec_helper/module_spec_helper' + +RSpec.configure do |config| + config.mock_with :rspec +end diff --git a/inifile/spec/unit/puppet/provider/ini_setting/inheritance_spec.rb b/inifile/spec/unit/puppet/provider/ini_setting/inheritance_spec.rb new file mode 100644 index 000000000..4dd6c3f5b --- /dev/null +++ b/inifile/spec/unit/puppet/provider/ini_setting/inheritance_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +# This is a reduced version of ruby_spec.rb just to ensure we can subclass as +# documented +$: << 'spec/fixtures/modules/inherit_ini_setting/lib' +provider_class = Puppet::Type.type(:inherit_ini_setting).provider(:ini_setting) +describe provider_class do + include PuppetlabsSpec::Files + + let(:tmpfile) { tmpfilename('inherit_ini_setting_test') } + + def validate_file(expected_content,tmpfile = tmpfile) + File.read(tmpfile).should == expected_content + end + + + before :each do + File.open(tmpfile, 'w') do |fh| + fh.write(orig_content) + end + end + + context 'when calling instances' do + let(:orig_content) { '' } + + it 'should parse nothing when the file is empty' do + provider_class.stubs(:file_path).returns(tmpfile) + provider_class.instances.should == [] + end + + context 'when the file has contents' do + let(:orig_content) { + <<-EOS +# A comment +red = blue +green = purple + EOS + } + + it 'should parse the results' do + provider_class.stubs(:file_path).returns(tmpfile) + instances = provider_class.instances + instances.size.should == 2 + # inherited version of namevar flattens the names + names = instances.map do |instance| + instance.instance_variable_get(:@property_hash)[:name] + end + names.sort.should == [ 'green', 'red' ] + end + end + end + + context 'when ensuring that a setting is present' do + let(:orig_content) { '' } + + it 'should add a value to the file' do + provider_class.stubs(:file_path).returns(tmpfile) + resource = Puppet::Type::Inherit_ini_setting.new({ + :setting => 'set_this', + :value => 'to_that', + }) + provider = described_class.new(resource) + provider.create + validate_file("set_this=to_that\n") + end + end +end diff --git a/inifile/spec/unit/puppet/provider/ini_setting/ruby_spec.rb b/inifile/spec/unit/puppet/provider/ini_setting/ruby_spec.rb new file mode 100644 index 000000000..550dd72cd --- /dev/null +++ b/inifile/spec/unit/puppet/provider/ini_setting/ruby_spec.rb @@ -0,0 +1,970 @@ +require 'spec_helper' +require 'puppet' + +provider_class = Puppet::Type.type(:ini_setting).provider(:ruby) +describe provider_class do + include PuppetlabsSpec::Files + + let(:tmpfile) { tmpfilename("ini_setting_test") } + let(:emptyfile) { tmpfilename("ini_setting_test_empty") } + + let(:common_params) { { + :title => 'ini_setting_ensure_present_test', + :path => tmpfile, + :section => 'section2', + } } + + def validate_file(expected_content,tmpfile = tmpfile) + File.read(tmpfile).should == expected_content + end + + + before :each do + File.open(tmpfile, 'w') do |fh| + fh.write(orig_content) + end + File.open(emptyfile, 'w') do |fh| + fh.write("") + end + end + + context 'when calling instances' do + + let :orig_content do + '' + end + + it 'should fail when file path is not set' do + expect { + provider_class.instances + }.to raise_error(Puppet::Error, 'Ini_settings only support collecting instances when a file path is hard coded') + end + + context 'when file path is set by a child class' do + it 'should return [] when file is empty' do + child_one = Class.new(provider_class) do + def self.file_path + emptyfile + end + end + child_one.stubs(:file_path).returns(emptyfile) + child_one.instances.should == [] + end + it 'should override the provider instances file_path' do + child_two = Class.new(provider_class) do + def self.file_path + '/some/file/path' + end + end + resource = Puppet::Type::Ini_setting.new(common_params) + provider = child_two.new(resource) + provider.file_path.should == '/some/file/path' + end + context 'when file has contecnts' do + let(:orig_content) { + <<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment + EOS + } + + it 'should be able to parse the results' do + child_three = Class.new(provider_class) do + def self.file_path + '/some/file/path' + end + end + child_three.stubs(:file_path).returns(tmpfile) + child_three.instances.size == 7 + expected_array = [ + {:name => 'section1/foo', :value => 'foovalue' }, + {:name => 'section1/bar', :value => 'barvalue' }, + {:name => 'section1/master', :value => 'true' }, + {:name => 'section2/foo', :value => 'foovalue2' }, + {:name => 'section2/baz', :value => 'bazvalue' }, + {:name => 'section2/url', :value => 'http://192.168.1.1:8080' }, + {:name => 'section:sub/subby', :value => 'bar' } + ] + real_array = [] + ensure_array = [] + child_three.instances.each do |x| + prop_hash = x.instance_variable_get(:@property_hash) + ensure_value = prop_hash.delete(:ensure) + ensure_array.push(ensure_value) + real_array.push(prop_hash) + end + ensure_array.uniq.should == [:present] + ((real_array - expected_array) && (expected_array - real_array)).should == [] + + end + + end + + end + + end + + context "when ensuring that a setting is present" do + let(:orig_content) { + <<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment + EOS + } + + it "should add a missing setting to the correct section" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :setting => 'yahoo', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +yahoo = yippee +[section:sub] +subby=bar + #another comment + ; yet another comment + EOS +) + end + + it "should add a missing setting to the correct section with colon" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section:sub', :setting => 'yahoo', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment +yahoo = yippee + EOS +) + end + + it "should modify an existing setting with a different value" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :setting => 'baz', :value => 'bazvalue2')) + provider = described_class.new(resource) + provider.exists?.should == 'bazvalue' + provider.value=('bazvalue2') + validate_file(<<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue2 +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment + EOS + ) + end + + it "should modify an existing setting with a different value - with colon in section" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section:sub', :setting => 'subby', :value => 'foo')) + provider = described_class.new(resource) + provider.exists?.should == 'bar' + provider.value.should == 'bar' + provider.value=('foo') + validate_file(<<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=foo + #another comment + ; yet another comment + EOS + ) + end + + it "should be able to handle settings with non alphanumbering settings " do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :setting => 'url', :value => 'http://192.168.0.1:8080')) + provider = described_class.new(resource) + provider.exists?.should == 'http://192.168.1.1:8080' + provider.value.should == 'http://192.168.1.1:8080' + provider.value=('http://192.168.0.1:8080') + + validate_file( <<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.0.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment + EOS + ) + end + + it "should recognize an existing setting with the specified value" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :setting => 'baz', :value => 'bazvalue')) + provider = described_class.new(resource) + provider.exists?.should == 'bazvalue' + end + + it "should add a new section if the section does not exist" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => "section3", :setting => 'huzzah', :value => 'shazaam')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment + +[section3] +huzzah = shazaam + EOS + ) + end + + it "should add a new section if the section does not exist - with colon" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => "section:subsection", :setting => 'huzzah', :value => 'shazaam')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment + +[section:subsection] +huzzah = shazaam + EOS + ) + end + + it "should add a new section if no sections exists" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => "section1", :setting => 'setting1', :value => 'hellowworld', :path => emptyfile)) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(" +[section1] +setting1 = hellowworld +", emptyfile) + end + + it "should add a new section with colon if no sections exists" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => "section:subsection", :setting => 'setting1', :value => 'hellowworld', :path => emptyfile)) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(" +[section:subsection] +setting1 = hellowworld +", emptyfile) + end + + it "should be able to handle variables of any type" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => "section1", :setting => 'master', :value => true)) + provider = described_class.new(resource) + provider.exists?.should == 'true' + provider.value.should == 'true' + end + + end + + context "when dealing with a global section" do + let(:orig_content) { + <<-EOS +# This is a comment +foo=blah +[section2] +foo = http://192.168.1.1:8080 + ; yet another comment + EOS + } + + + it "should add a missing setting if it doesn't exist" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => '', :setting => 'bar', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +# This is a comment +foo=blah +bar = yippee +[section2] +foo = http://192.168.1.1:8080 + ; yet another comment + EOS + ) + end + + it "should modify an existing setting with a different value" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => '', :setting => 'foo', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should == 'blah' + provider.value.should == 'blah' + provider.value=('yippee') + validate_file(<<-EOS +# This is a comment +foo=yippee +[section2] +foo = http://192.168.1.1:8080 + ; yet another comment + EOS + ) + end + + it "should recognize an existing setting with the specified value" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => '', :setting => 'foo', :value => 'blah')) + provider = described_class.new(resource) + provider.exists?.should == 'blah' + end + end + + context "when the first line of the file is a section" do + let(:orig_content) { + <<-EOS +[section2] +foo = http://192.168.1.1:8080 + EOS + } + + it "should be able to add a global setting" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => '', :setting => 'foo', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +foo = yippee + +[section2] +foo = http://192.168.1.1:8080 + EOS + ) + end + + it "should modify an existing setting" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section2', :setting => 'foo', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should == 'http://192.168.1.1:8080' + provider.value.should == 'http://192.168.1.1:8080' + provider.value=('yippee') + validate_file(<<-EOS +[section2] +foo = yippee + EOS + ) + end + + it "should add a new setting" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section2', :setting => 'bar', :value => 'baz')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +[section2] +foo = http://192.168.1.1:8080 +bar = baz + EOS + ) + end + end + + context "when overriding the separator" do + let(:orig_content) { + <<-EOS +[section2] +foo=bar + EOS + } + + it "should fail if the separator doesn't include an equals sign" do + expect { + Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section2', + :setting => 'foo', + :value => 'yippee', + :key_val_separator => '+')) + }.to raise_error Puppet::Error, /must contain exactly one/ + end + + it "should fail if the separator includes more than one equals sign" do + expect { + Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section2', + :setting => 'foo', + :value => 'yippee', + :key_val_separator => ' = foo = ')) + }.to raise_error Puppet::Error, /must contain exactly one/ + end + + it "should modify an existing setting" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section2', + :setting => 'foo', + :value => 'yippee', + :key_val_separator => '=')) + provider = described_class.new(resource) + provider.exists?.should == 'bar' + provider.value.should == 'bar' + provider.value=('yippee') + validate_file(<<-EOS +[section2] +foo=yippee + EOS + ) + end + + it "should add a new setting" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section2', + :setting => 'bar', + :value => 'baz', + :key_val_separator => '=')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +[section2] +foo=bar +bar=baz + EOS + ) + end + + end + + context "when ensuring that a setting is absent" do + let(:orig_content) { + <<-EOS +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment +EOS + } + + it "should remove a setting that exists" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section1', :setting => 'foo', :ensure => 'absent')) + provider = described_class.new(resource) + provider.exists?.should be_true + provider.destroy + validate_file(<<-EOS +[section1] +; This is also a comment + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment +EOS + ) + end + + it "should do nothing for a setting that does not exist" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section:sub', :setting => 'foo', :ensure => 'absent')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.destroy + validate_file(<<-EOS +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +master = true +[section2] + +foo= foovalue2 +baz=bazvalue +url = http://192.168.1.1:8080 +[section:sub] +subby=bar + #another comment + ; yet another comment + EOS + ) + end + end + + + context "when dealing with indentation in sections" do + let(:orig_content) { + <<-EOS +# This is a comment + [section1] + ; This is also a comment + foo=foovalue + + bar = barvalue + master = true + +[section2] + foo= foovalue2 + baz=bazvalue + url = http://192.168.1.1:8080 +[section:sub] + subby=bar + #another comment + fleezy = flam + ; yet another comment + EOS + } + + it "should add a missing setting at the correct indentation when the header is aligned" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section1', :setting => 'yahoo', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +# This is a comment + [section1] + ; This is also a comment + foo=foovalue + + bar = barvalue + master = true + yahoo = yippee + +[section2] + foo= foovalue2 + baz=bazvalue + url = http://192.168.1.1:8080 +[section:sub] + subby=bar + #another comment + fleezy = flam + ; yet another comment + EOS + ) + end + + it "should update an existing setting at the correct indentation when the header is aligned" do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section1', :setting => 'bar', :value => 'barvalue2')) + provider = described_class.new(resource) + provider.exists?.should be_true + provider.create + validate_file(<<-EOS +# This is a comment + [section1] + ; This is also a comment + foo=foovalue + + bar = barvalue2 + master = true + +[section2] + foo= foovalue2 + baz=bazvalue + url = http://192.168.1.1:8080 +[section:sub] + subby=bar + #another comment + fleezy = flam + ; yet another comment + EOS + ) + end + + it "should add a missing setting at the correct indentation when the header is not aligned" do + resource = Puppet::Type::Ini_setting.new(common_params.merge( + :section => 'section2', :setting => 'yahoo', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +# This is a comment + [section1] + ; This is also a comment + foo=foovalue + + bar = barvalue + master = true + +[section2] + foo= foovalue2 + baz=bazvalue + url = http://192.168.1.1:8080 + yahoo = yippee +[section:sub] + subby=bar + #another comment + fleezy = flam + ; yet another comment + EOS + ) + end + + it "should update an existing setting at the correct indentation when the header is not aligned" do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section2', :setting => 'baz', :value => 'bazvalue2')) + provider = described_class.new(resource) + provider.exists?.should be_true + provider.create + validate_file(<<-EOS +# This is a comment + [section1] + ; This is also a comment + foo=foovalue + + bar = barvalue + master = true + +[section2] + foo= foovalue2 + baz=bazvalue2 + url = http://192.168.1.1:8080 +[section:sub] + subby=bar + #another comment + fleezy = flam + ; yet another comment + EOS + ) + end + + it "should add a missing setting at the min indentation when the section is not aligned" do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section:sub', :setting => 'yahoo', :value => 'yippee')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +# This is a comment + [section1] + ; This is also a comment + foo=foovalue + + bar = barvalue + master = true + +[section2] + foo= foovalue2 + baz=bazvalue + url = http://192.168.1.1:8080 +[section:sub] + subby=bar + #another comment + fleezy = flam + ; yet another comment + yahoo = yippee + EOS + ) + end + + it "should update an existing setting at the previous indentation when the section is not aligned" do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section:sub', :setting => 'fleezy', :value => 'flam2')) + provider = described_class.new(resource) + provider.exists?.should be_true + provider.create + validate_file(<<-EOS +# This is a comment + [section1] + ; This is also a comment + foo=foovalue + + bar = barvalue + master = true + +[section2] + foo= foovalue2 + baz=bazvalue + url = http://192.168.1.1:8080 +[section:sub] + subby=bar + #another comment + fleezy = flam2 + ; yet another comment + EOS + ) + end + + end + + + context "when dealing settings that have a commented version present" do + let(:orig_content) { + <<-EOS + [section1] + # foo=foovalue + bar=barvalue + foo = foovalue2 + +[section2] +# foo = foovalue +;bar=barvalue +blah = blah +#baz= + EOS + } + + it "should add a new setting below a commented version of that setting" do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section2', :setting => 'foo', :value => 'foo3')) + provider = described_class.new(resource) + provider.exists?.should be_false + provider.create + validate_file(<<-EOS + [section1] + # foo=foovalue + bar=barvalue + foo = foovalue2 + +[section2] +# foo = foovalue +foo = foo3 +;bar=barvalue +blah = blah +#baz= + EOS + ) + end + + it "should update an existing setting in place, even if there is a commented version of that setting" do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section1', :setting => 'foo', :value => 'foo3')) + provider = described_class.new(resource) + provider.exists?.should be_true + provider.create + validate_file(<<-EOS + [section1] + # foo=foovalue + bar=barvalue + foo = foo3 + +[section2] +# foo = foovalue +;bar=barvalue +blah = blah +#baz= + EOS + ) + end + + it "should add a new setting below a commented version of that setting, respecting semicolons as comments" do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section2', :setting => 'bar', :value => 'bar3')) + provider = described_class.new(resource) + provider.exists?.should be_false + provider.create + validate_file(<<-EOS + [section1] + # foo=foovalue + bar=barvalue + foo = foovalue2 + +[section2] +# foo = foovalue +;bar=barvalue +bar=bar3 +blah = blah +#baz= + EOS + ) + end + + it "should add a new setting below an empty commented version of that setting" do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section2', :setting => 'baz', :value => 'bazvalue')) + provider = described_class.new(resource) + provider.exists?.should be_false + provider.create + validate_file(<<-EOS + [section1] + # foo=foovalue + bar=barvalue + foo = foovalue2 + +[section2] +# foo = foovalue +;bar=barvalue +blah = blah +#baz= +baz=bazvalue + EOS + ) + end + + context 'when a section only contains comments' do + let(:orig_content) { + <<-EOS +[section1] +# foo=foovalue +# bar=bar2 +EOS + } + it 'should be able to add a new setting when a section contains only comments' do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section1', :setting => 'foo', :value => 'foovalue2') + ) + provider = described_class.new(resource) + provider.exists?.should be_false + provider.create + validate_file(<<-EOS +[section1] +# foo=foovalue +foo=foovalue2 +# bar=bar2 + EOS + ) + end + it 'should be able to add a new setting when it matches a commented out line other than the first one' do + resource = Puppet::Type::Ini_setting.new( + common_params.merge(:section => 'section1', :setting => 'bar', :value => 'barvalue2') + ) + provider = described_class.new(resource) + provider.exists?.should be_false + provider.create + validate_file(<<-EOS +[section1] +# foo=foovalue +# bar=bar2 +bar=barvalue2 + EOS + ) + end + end + + end + +end diff --git a/inifile/spec/unit/puppet/provider/ini_subsetting/ruby_spec.rb b/inifile/spec/unit/puppet/provider/ini_subsetting/ruby_spec.rb new file mode 100644 index 000000000..ebe55c09c --- /dev/null +++ b/inifile/spec/unit/puppet/provider/ini_subsetting/ruby_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' +require 'puppet' + +provider_class = Puppet::Type.type(:ini_subsetting).provider(:ruby) +describe provider_class do + include PuppetlabsSpec::Files + + let(:tmpfile) { tmpfilename("ini_setting_test") } + + def validate_file(expected_content,tmpfile = tmpfile) + File.read(tmpfile).should == expected_content + end + + + before :each do + File.open(tmpfile, 'w') do |fh| + fh.write(orig_content) + end + end + + context "when ensuring that a subsetting is present" do + let(:common_params) { { + :title => 'ini_setting_ensure_present_test', + :path => tmpfile, + :section => '', + :key_val_separator => '=', + :setting => 'JAVA_ARGS', + } } + + let(:orig_content) { + <<-EOS +JAVA_ARGS="-Xmx192m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/pe-puppetdb/puppetdb-oom.hprof" + EOS + } + + it "should add a missing subsetting" do + resource = Puppet::Type::Ini_subsetting.new(common_params.merge( + :subsetting => '-Xms', :value => '128m')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.create + validate_file(<<-EOS +JAVA_ARGS="-Xmx192m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/pe-puppetdb/puppetdb-oom.hprof -Xms128m" + EOS +) + end + + it "should remove an existing subsetting" do + resource = Puppet::Type::Ini_subsetting.new(common_params.merge( + :subsetting => '-Xmx')) + provider = described_class.new(resource) + provider.exists?.should == "192m" + provider.destroy + validate_file(<<-EOS +JAVA_ARGS="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/pe-puppetdb/puppetdb-oom.hprof" + EOS +) + end + + it "should modify an existing subsetting" do + resource = Puppet::Type::Ini_subsetting.new(common_params.merge( + :subsetting => '-Xmx', :value => '256m')) + provider = described_class.new(resource) + provider.exists?.should == "192m" + provider.value=('256m') + validate_file(<<-EOS +JAVA_ARGS="-Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/pe-puppetdb/puppetdb-oom.hprof" + EOS +) + end + end + + context "when working with subsettings in files with unquoted settings values" do + let(:common_params) { { + :title => 'ini_setting_ensure_present_test', + :path => tmpfile, + :section => 'master', + :setting => 'reports', + } } + + let(:orig_content) { + <<-EOS +[master] + +reports = http,foo + EOS + } + + it "should remove an existing subsetting" do + resource = Puppet::Type::Ini_subsetting.new(common_params.merge( + :subsetting => 'http', :subsetting_separator => ',')) + provider = described_class.new(resource) + provider.exists?.should == "" + provider.destroy + validate_file(<<-EOS +[master] + +reports = foo + EOS + ) + end + + it "should add a new subsetting when the 'parent' setting already exists" do + resource = Puppet::Type::Ini_subsetting.new(common_params.merge( + :subsetting => 'puppetdb', :subsetting_separator => ',')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.value=('') + validate_file(<<-EOS +[master] + +reports = http,foo,puppetdb + EOS + ) + end + + it "should add a new subsetting when the 'parent' setting does not already exist" do + resource = Puppet::Type::Ini_subsetting.new(common_params.merge( + :setting => 'somenewsetting', + :subsetting => 'puppetdb', + :subsetting_separator => ',')) + provider = described_class.new(resource) + provider.exists?.should be_nil + provider.value=('') + validate_file(<<-EOS +[master] + +reports = http,foo +somenewsetting = puppetdb + EOS + ) + end + + end +end diff --git a/inifile/spec/unit/puppet/util/external_iterator_spec.rb b/inifile/spec/unit/puppet/util/external_iterator_spec.rb new file mode 100644 index 000000000..92b17af44 --- /dev/null +++ b/inifile/spec/unit/puppet/util/external_iterator_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +require 'puppet/util/external_iterator' + +describe Puppet::Util::ExternalIterator do + let(:subject) { Puppet::Util::ExternalIterator.new(["a", "b", "c"]) } + + context "#next" do + it "should iterate over the items" do + subject.next.should == ["a", 0] + subject.next.should == ["b", 1] + subject.next.should == ["c", 2] + end + end + + context "#peek" do + it "should return the 0th item repeatedly" do + subject.peek.should == ["a", 0] + subject.peek.should == ["a", 0] + end + + it "should not advance the iterator, but should reflect calls to #next" do + subject.peek.should == ["a", 0] + subject.peek.should == ["a", 0] + subject.next.should == ["a", 0] + subject.peek.should == ["b", 1] + subject.next.should == ["b", 1] + subject.peek.should == ["c", 2] + subject.next.should == ["c", 2] + subject.peek.should == [nil, nil] + subject.next.should == [nil, nil] + end + end + + +end diff --git a/inifile/spec/unit/puppet/util/ini_file_spec.rb b/inifile/spec/unit/puppet/util/ini_file_spec.rb new file mode 100644 index 000000000..8ab18cf24 --- /dev/null +++ b/inifile/spec/unit/puppet/util/ini_file_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' +require 'stringio' +require 'puppet/util/ini_file' + +describe Puppet::Util::IniFile do + let(:subject) { Puppet::Util::IniFile.new("/my/ini/file/path") } + + before :each do + File.should_receive(:file?).with("/my/ini/file/path") { true } + described_class.should_receive(:readlines).once.with("/my/ini/file/path") do + sample_content + end + end + + context "when parsing a file" do + let(:sample_content) { + template = <<-EOS +# This is a comment +[section1] +; This is also a comment +foo=foovalue + +bar = barvalue +baz = +[section2] + +foo= foovalue2 +baz=bazvalue + #another comment + ; yet another comment + zot = multi word value + EOS + template.split("\n") + } + + it "should parse the correct number of sections" do + # there is always a "global" section, so our count should be 3. + subject.section_names.length.should == 3 + end + + it "should parse the correct section_names" do + # there should always be a "global" section named "" at the beginning of the list + subject.section_names.should == ["", "section1", "section2"] + end + + it "should expose settings for sections" do + subject.get_value("section1", "foo").should == "foovalue" + subject.get_value("section1", "bar").should == "barvalue" + subject.get_value("section1", "baz").should == "" + subject.get_value("section2", "foo").should == "foovalue2" + subject.get_value("section2", "baz").should == "bazvalue" + subject.get_value("section2", "zot").should == "multi word value" + end + + end + + context "when parsing a file whose first line is a section" do + let(:sample_content) { + template = <<-EOS +[section1] +; This is a comment +foo=foovalue + EOS + template.split("\n") + } + + it "should parse the correct number of sections" do + # there is always a "global" section, so our count should be 2. + subject.section_names.length.should == 2 + end + + it "should parse the correct section_names" do + # there should always be a "global" section named "" at the beginning of the list + subject.section_names.should == ["", "section1"] + end + + it "should expose settings for sections" do + subject.get_value("section1", "foo").should == "foovalue" + end + + end + + context "when parsing a file with a 'global' section" do + let(:sample_content) { + template = <<-EOS +foo = bar +[section1] +; This is a comment +foo=foovalue + EOS + template.split("\n") + } + + it "should parse the correct number of sections" do + # there is always a "global" section, so our count should be 2. + subject.section_names.length.should == 2 + end + + it "should parse the correct section_names" do + # there should always be a "global" section named "" at the beginning of the list + subject.section_names.should == ["", "section1"] + end + + it "should expose settings for sections" do + subject.get_value("", "foo").should == "bar" + subject.get_value("section1", "foo").should == "foovalue" + end + end + + context "when updating a file with existing empty values" do + let(:sample_content) { + template = <<-EOS +[section1] +foo= +#bar= + EOS + template.split("\n") + } + + it "should properly update uncommented values" do + subject.get_value("section1", "far").should == nil + subject.set_value("section1", "foo", "foovalue") + subject.get_value("section1", "foo").should == "foovalue" + end + + it "should properly update commented values" do + subject.get_value("section1", "bar").should == nil + subject.set_value("section1", "bar", "barvalue") + subject.get_value("section1", "bar").should == "barvalue" + end + + it "should properly add new empty values" do + subject.get_value("section1", "baz").should == nil + subject.set_value("section1", "baz", "bazvalue") + subject.get_value("section1", "baz").should == "bazvalue" + end + end +end diff --git a/inifile/spec/unit/puppet/util/setting_value_spec.rb b/inifile/spec/unit/puppet/util/setting_value_spec.rb new file mode 100644 index 000000000..61483968a --- /dev/null +++ b/inifile/spec/unit/puppet/util/setting_value_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' +require 'puppet/util/setting_value' + +describe Puppet::Util::SettingValue do + + describe "space subsetting separator" do + INIT_VALUE_SPACE = "\"-Xmx192m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/pe-puppetdb/puppetdb-oom.hprof\"" + + before :each do + @setting_value = Puppet::Util::SettingValue.new(INIT_VALUE_SPACE, " ") + end + + it "should get the original value" do + @setting_value.get_value.should == INIT_VALUE_SPACE + end + + it "should get the correct value" do + @setting_value.get_subsetting_value("-Xmx").should == "192m" + end + + it "should add a new value" do + @setting_value.add_subsetting("-Xms", "256m") + @setting_value.get_subsetting_value("-Xms").should == "256m" + @setting_value.get_value.should == INIT_VALUE_SPACE[0, INIT_VALUE_SPACE.length - 1] + " -Xms256m\"" + end + + it "should change existing value" do + @setting_value.add_subsetting("-Xmx", "512m") + @setting_value.get_subsetting_value("-Xmx").should == "512m" + end + + it "should remove existing value" do + @setting_value.remove_subsetting("-Xmx") + @setting_value.get_subsetting_value("-Xmx").should == nil + end + end + + describe "comma subsetting separator" do + INIT_VALUE_COMMA = "\"-Xmx192m,-XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath=/var/log/pe-puppetdb/puppetdb-oom.hprof\"" + + before :each do + @setting_value = Puppet::Util::SettingValue.new(INIT_VALUE_COMMA, ",") + end + + it "should get the original value" do + @setting_value.get_value.should == INIT_VALUE_COMMA + end + + it "should get the correct value" do + @setting_value.get_subsetting_value("-Xmx").should == "192m" + end + + it "should add a new value" do + @setting_value.add_subsetting("-Xms", "256m") + @setting_value.get_subsetting_value("-Xms").should == "256m" + @setting_value.get_value.should == INIT_VALUE_COMMA[0, INIT_VALUE_COMMA.length - 1] + ",-Xms256m\"" + end + + it "should change existing value" do + @setting_value.add_subsetting("-Xmx", "512m") + @setting_value.get_subsetting_value("-Xmx").should == "512m" + end + + it "should remove existing value" do + @setting_value.remove_subsetting("-Xmx") + @setting_value.get_subsetting_value("-Xmx").should == nil + end + end +end diff --git a/inifile/tests/ini_setting.pp b/inifile/tests/ini_setting.pp new file mode 100644 index 000000000..279cb00b6 --- /dev/null +++ b/inifile/tests/ini_setting.pp @@ -0,0 +1,25 @@ +ini_setting { 'sample setting': + ensure => present, + path => '/tmp/foo.ini', + section => 'foo', + setting => 'foosetting', + value => 'FOO!', +} + +ini_setting { 'sample setting2': + ensure => present, + path => '/tmp/foo.ini', + section => 'bar', + setting => 'barsetting', + value => 'BAR!', + key_val_separator => '=', + require => Ini_setting['sample setting'], +} + +ini_setting { 'sample setting3': + ensure => absent, + path => '/tmp/foo.ini', + section => 'bar', + setting => 'bazsetting', + require => Ini_setting['sample setting2'], +} diff --git a/inifile/tests/ini_subsetting.pp b/inifile/tests/ini_subsetting.pp new file mode 100644 index 000000000..0458354ac --- /dev/null +++ b/inifile/tests/ini_subsetting.pp @@ -0,0 +1,18 @@ +ini_subsetting { 'sample subsetting': + ensure => present, + section => '', + key_val_separator => '=', + path => '/etc/default/pe-puppetdb', + setting => 'JAVA_ARGS', + subsetting => '-Xmx', + value => '512m', +} + +ini_subsetting { 'sample subsetting2': + ensure => absent, + section => '', + key_val_separator => '=', + path => '/etc/default/pe-puppetdb', + setting => 'JAVA_ARGS', + subsetting => '-Xms', +} diff --git a/keystone b/keystone deleted file mode 160000 index 605161f3d..000000000 --- a/keystone +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 605161f3d4b7bbcffc657c86b367159701dfdcbe diff --git a/keystone/.fixtures.yml b/keystone/.fixtures.yml new file mode 100644 index 000000000..34dd37c04 --- /dev/null +++ b/keystone/.fixtures.yml @@ -0,0 +1,16 @@ +fixtures: + repositories: + 'apache': 'git://github.com/puppetlabs/puppetlabs-apache.git' + 'concat': 'git://github.com/puppetlabs/puppetlabs-concat.git' + 'apt': 'git://github.com/puppetlabs/puppetlabs-apt.git' + 'mysql': + repo: 'git://github.com/puppetlabs/puppetlabs-mysql.git' + ref: 'origin/2.2.x' + 'openstacklib': 'git://github.com/stackforge/puppet-openstacklib.git' + 'stdlib': 'git://github.com/puppetlabs/puppetlabs-stdlib.git' + 'inifile': 'git://github.com/puppetlabs/puppetlabs-inifile' + 'postgresql': + repo: 'git://github.com/puppetlabs/puppetlabs-postgresql.git' + ref: '2.5.0' + symlinks: + 'keystone': "#{source_dir}" diff --git a/keystone/.gitignore b/keystone/.gitignore new file mode 100644 index 000000000..f67fd0665 --- /dev/null +++ b/keystone/.gitignore @@ -0,0 +1,5 @@ +spec/fixtures/modules/* +spec/fixtures/manifests/* +pkg +Gemfile.lock +*.swp diff --git a/keystone/.gitreview b/keystone/.gitreview new file mode 100644 index 000000000..23429b728 --- /dev/null +++ b/keystone/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=stackforge/puppet-keystone.git diff --git a/keystone/Gemfile b/keystone/Gemfile new file mode 100644 index 000000000..0d35201b4 --- /dev/null +++ b/keystone/Gemfile @@ -0,0 +1,15 @@ +source 'https://rubygems.org' + +group :development, :test do + gem 'puppetlabs_spec_helper', :require => false + gem 'puppet-lint', '~> 0.3.2' + gem 'rake', '10.1.1' +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/keystone/LICENSE b/keystone/LICENSE new file mode 100644 index 000000000..0bc44c17d --- /dev/null +++ b/keystone/LICENSE @@ -0,0 +1,17 @@ +Puppet Labs Keystone Module - Puppet module for managing Keystone + +Copyright (C) 2012 Puppet Labs Inc + +Puppet Labs can be contacted at: info@puppetlabs.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/keystone/Modulefile b/keystone/Modulefile new file mode 100644 index 000000000..27670233a --- /dev/null +++ b/keystone/Modulefile @@ -0,0 +1,13 @@ +name 'puppetlabs-keystone' +version '4.0.0' +author 'Puppet Labs and StackForge Contributors' +license 'Apache License 2.0' +summary 'Puppet module for OpenStack Keystone' +description 'Installs and configures OpenStack Keystone (Identity).' +project_page 'https://launchpad.net/puppet-keystone' +source 'https://github.com/stackforge/puppet-keystone' + +dependency 'puppetlabs/apache', '>=1.0.0 <2.0.0' +dependency 'puppetlabs/inifile', '>=1.0.0 <2.0.0' +dependency 'puppetlabs/stdlib', '>= 3.2.0' +dependency 'stackforge/openstacklib', '>=5.0.0' diff --git a/keystone/README.md b/keystone/README.md new file mode 100644 index 000000000..079b648c3 --- /dev/null +++ b/keystone/README.md @@ -0,0 +1,235 @@ +keystone +======= + +4.0.0 - 2014.1.0 - Icehouse + +#### Table of Contents + +1. [Overview - What is the keystone module?](#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with keystone](#setup) +4. [Implementation - An under-the-hood peek at what the module is doing](#implementation) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) +7. [Contributors - Those with commits](#contributors) +8. [Release Notes - Notes on the most recent updates to the module](#release-notes) + +Overview +-------- + +The keystone module is a part of [Stackforge](https://github.com/stackfoge), an effort by the Openstack infrastructure team to provide continuous integration testing and code review for Openstack and Openstack community projects not part of the core software. The module its self is used to flexibly configure and manage the identify service for Openstack. + +Module Description +------------------ + +The keystone module is a thorough attempt to make Puppet capable of managing the entirety of keystone. This includes manifests to provision region specific endpoint and database connections. Types are shipped as part of the keystone module to assist in manipulation of configuration files. + +This module is tested in combination with other modules needed to build and leverage an entire Openstack software stack. These modules can be found, all pulled together in the [openstack module](https://github.com/stackfoge/puppet-openstack). + +Setup +----- + +**What the keystone module affects** + +* keystone, the identify service for Openstack. + +### Installing keystone + + example% puppet module install puppetlabs/keystone + +### Beginning with keystone + +To utilize the keystone module's functionality you will need to declare multiple resources. The following is a modified excerpt from the [openstack module](https://github.com/stackfoge/puppet-openstack). This is not an exhaustive list of all the components needed, we recommend you consult and understand the [openstack module](https://github.com/stackforge/puppet-openstack) and the [core openstack](http://docs.openstack.org) documentation. + +**Define a keystone node** + +```puppet +class { 'keystone': + verbose => True, + catalog_type => 'sql', + admin_token => 'random_uuid', + sql_connection => 'mysql://keystone_admin:super_secret_db_password@openstack-controller.example.com/keystone', +} + +# Adds the admin credential to keystone. +class { 'keystone::roles::admin': + email => 'admin@example.com', + password => 'super_secret', +} + +# Installs the service user endpoint. +class { 'keystone::endpoint': + public_address => '10.16.0.101', + admin_address => '10.16.1.101', + internal_address => '10.16.2.101', + region => 'example-1', +} +``` + +**Leveraging the Native Types** + +Keystone ships with a collection of native types that can be used to interact with the data stored in keystone. The following, related to user management could live throughout your Puppet code base. They even support puppet's ability to introspect the current environment much the same as `puppet resource user`, `puppet resouce keystone_tenant` will print out all the currently stored tenants and their parameters. + +```puppet +keystone_tenant { 'openstack': + ensure => present, + enabled => True, +} +keystone_user { 'openstack': + ensure => present, + enabled => True, +} +keystone_role { 'admin': + ensure => present, +} +keystone_user_role { 'admin@openstack': + roles => ['admin', 'superawesomedude'], + ensure => present +} +``` + +These two will seldom be used outside openstack related classes, like nova or cinder. These are modified examples form Class['nova::keystone::auth']. + +```puppet +# Setup the nova keystone service +keystone_service { 'nova': + ensure => present, + type => 'compute', + description => 'Openstack Compute Service', +} + +# Setup nova keystone endpoint +keystone_endpoint { 'example-1-west/nova': + ensure => present, + public_url => "http://127.0.0.1:8774/v2/%(tenant_id)s", + admin_url => "http://127.0.0.1:8774/v2/%(tenant_id)s", + internal_url => "http://127.0.0.1:8774/v2/%(tenant_id)s", +} +``` + +**Setting up a database for keystone** + +A keystone database can be configured separately from the keystone services. + +If one needs to actually install a fresh database they have the choice of mysql or postgres. Use the mysql::server or postgreql::server classes to do this setup then the Class['keystone::db::mysql'] or Class['keystone::db::postgresql'] for adding the needed databases and users that will be needed by keystone. + +* For mysql + +```puppet +class { 'mysql::server': } + +class { 'keystone::db::mysql': + password => 'super_secret_db_password', + allowed_hosts => '%', +} +``` + +* For postgresql + +```puppet +class { 'postgresql::server': } + +class { 'keystone::db::postgresql': password => 'super_secret_db_password', } +``` + +Implementation +-------------- + +### keystone + +keystone is a combination of Puppet manifest and ruby code to delivery configuration and extra functionality through types and providers. + +Limitations +------------ + +* All the keystone types use the CLI tools and so need to be ran on the keystone node. + +### Upgrade warning + +* If you've setup Openstack using previous versions of this module you need to be aware that it used UUID as the dedault to the token_format parameter but now defaults to PKI. If you're using this module to manage a Grizzly Openstack deployment that was set up using a development release of the modules or are attempting an upgrade from Folsom then you'll need to make sure you set the token_format to UUID at classification time. + +Development +----------- + +Developer documentation for the entire puppet-openstack project. + +* https://wiki.openstack.org/wiki/Puppet-openstack#Developer_documentation + +Contributors +------------ + +* https://github.com/stackforge/puppet-keystone/graphs/contributors + +Release Notes +------------- + +**4.0.0** + +* Stable Icehouse release. +* Added template_file parameter to specify catalog. +* Added keystone::config to handle additional custom options. +* Added notification parameters. +* Added support for puppetlabs-mysql 2.2 and greater. +* Fixed deprecated sql section header in keystone.conf. +* Fixed deprecated bind_host parameter. +* Fixed example for native type keystone_service. +* Fixed LDAP module bugs. +* Fixed variable for host_access dependency. +* Reduced default token duration to one hour. + +**3.2.0** + +* Added ability to configure any catalog driver. +* Ensures log_file is absent when using syslog. + +**3.1.1** + +* Fixed inconsistent variable for mysql allowed hosts. + +**3.1.0** + +* Added ability to disable pki_setup. +* Load tenant un-lazily if needed. +* Add log_dir param, with option to disable. +* Updated endpoint argument. +* Added support to enable SSL. +* Removes setting of Keystone endpoint by default. +* Relaxed regex when keystone refuses connections. + +**3.0.0** + +* Major release for OpenStack Havana. +* Fixed duplicated keystone endpoints. +* Refactored keystone_endpoint to use prefetch and flush paradigm. +* Switched from signing/format to token/provider. +* Created memcache_servers option to allow for multiple cache servers. +* Enabled serving Keystone from Apache mod_wsgi. +* Moved db_sync to its own class. +* Removed creation of Member role. +* Improved performance of Keystone providers. +* Updated endpoints to support paths and ssl. +* Added support for token expiration parameter. + +**2.2.0** + +* Optimized tenant and user queries. +* Added syslog support. +* Added support for token driver backend. +* Various bug and lint fixes. + +**2.1.0** + +* Tracks release of puppet-quantum +* Fixed allowed_hosts contitional statement +* Pinned depedencies +* Select keystone endpoint based on SSL setting +* Improved tenant_hash usage in keystone_tenant +* Various cleanup and bug fixes. + +**2.0.0** + +* Upstream is now part of stackfoge. +* keystone_user can be used to change passwords. +* service tenant name now configurable. +* keystone_user is now idempotent. +* Various cleanups and bug fixes. diff --git a/keystone/Rakefile b/keystone/Rakefile new file mode 100644 index 000000000..b07ed10b2 --- /dev/null +++ b/keystone/Rakefile @@ -0,0 +1,7 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings = true +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_inherits_from_params_class') +PuppetLint.configuration.send('disable_class_parameter_defaults') diff --git a/keystone/examples/apache_dropin.pp b/keystone/examples/apache_dropin.pp new file mode 100644 index 000000000..310f0a3fd --- /dev/null +++ b/keystone/examples/apache_dropin.pp @@ -0,0 +1,49 @@ +# Example using apache to serve keystone +# +# To be sure everything is working, run: +# $ export OS_USERNAME=admin +# $ export OS_PASSWORD=ChangeMe +# $ export OS_TENANT_NAME=openstack +# $ export OS_AUTH_URL=http://keystone.local/keystone/main/v2.0 +# $ keystone catalog +# Service: identity +# +-------------+----------------------------------------------+ +# | Property | Value | +# +-------------+----------------------------------------------+ +# | adminURL | http://keystone.local:80/keystone/admin/v2.0 | +# | id | 4f0f55f6789d4c73a53c51f991559b72 | +# | internalURL | http://keystone.local:80/keystone/main/v2.0 | +# | publicURL | http://keystone.local:80/keystone/main/v2.0 | +# | region | RegionOne | +# +-------------+----------------------------------------------+ +# + +Exec { logoutput => 'on_failure' } + +class { 'mysql::server': } +class { 'keystone::db::mysql': + password => 'keystone', +} +class { 'keystone': + verbose => true, + debug => true, + sql_connection => 'mysql://keystone:keystone@127.0.0.1/keystone', + catalog_type => 'sql', + admin_token => 'admin_token', + enabled => false, +} +class { 'keystone::roles::admin': + email => 'test@puppetlabs.com', + password => 'ChangeMe', +} +class { 'keystone::endpoint': + public_url => "https://${::fqdn}:5000/", + admin_url => "https://${::fqdn}:35357/", +} + +keystone_config { 'ssl/enable': value => true } + +include apache +class { 'keystone::wsgi::apache': + ssl => true +} diff --git a/keystone/examples/apache_with_paths.pp b/keystone/examples/apache_with_paths.pp new file mode 100644 index 000000000..be28d395b --- /dev/null +++ b/keystone/examples/apache_with_paths.pp @@ -0,0 +1,54 @@ +# Example using apache to serve keystone +# +# To be sure everything is working, run: +# $ export OS_USERNAME=admin +# $ export OS_PASSWORD=ChangeMe +# $ export OS_TENANT_NAME=openstack +# $ export OS_AUTH_URL=http://keystone.local/keystone/main/v2.0 +# $ keystone catalog +# Service: identity +# +-------------+----------------------------------------------+ +# | Property | Value | +# +-------------+----------------------------------------------+ +# | adminURL | http://keystone.local:80/keystone/admin/v2.0 | +# | id | 4f0f55f6789d4c73a53c51f991559b72 | +# | internalURL | http://keystone.local:80/keystone/main/v2.0 | +# | publicURL | http://keystone.local:80/keystone/main/v2.0 | +# | region | RegionOne | +# +-------------+----------------------------------------------+ +# + +Exec { logoutput => 'on_failure' } + +class { 'mysql::server': } +class { 'keystone::db::mysql': + password => 'keystone', +} +class { 'keystone': + verbose => true, + debug => true, + sql_connection => 'mysql://keystone_admin:keystone@127.0.0.1/keystone', + catalog_type => 'sql', + admin_token => 'admin_token', + enabled => true, +} +class { 'keystone::cron::token_flush': } +class { 'keystone::roles::admin': + email => 'test@puppetlabs.com', + password => 'ChangeMe', +} +class { 'keystone::endpoint': + public_url => "https://${::fqdn}:443/main/", + admin_address => "https://${::fqdn}:443/admin/", +} + +keystone_config { 'ssl/enable': ensure => absent } + +include apache +class { 'keystone::wsgi::apache': + ssl => true, + public_port => 443, + admin_port => 443, + public_path => '/main/', + admin_path => '/admin/' +} diff --git a/keystone/examples/ldap_full.pp b/keystone/examples/ldap_full.pp new file mode 100644 index 000000000..d8f1d4113 --- /dev/null +++ b/keystone/examples/ldap_full.pp @@ -0,0 +1,66 @@ +# A full example from a real deployment that allows Keystone to modify +# everything except users, uses enabled_emulation, and ldaps + +# Ensure this matches what is in LDAP or keystone will try to recreate +# the admin user +class { 'keystone::roles::admin': + email => 'test@example.com', + password => 'ChangeMe', +} + +# You can test this connection with ldapsearch first to ensure it works. +# LDAP configurations are *highly* dependent on your setup and this file +# will need to be tweaked. This sample talks to ldap.example.com, here is +# an example of ldapsearch that will search users on this box: +# ldapsearch -v -x -H 'ldap://69.134.70.154:389' -D \ +# "uid=bind,cn=users,cn=accounts,dc=example,dc=com" -w SecretPass \ +# -b cn=users,cn=accounts,dc=example,dc=com +class { 'keystone:ldap': + url => 'ldap://ldap.example.com:389', + user => 'uid=bind,cn=users,cn=accounts,dc=example,dc=com', + password => 'SecretPass', + suffix => 'dc=example,dc=com', + query_scope => 'sub', + user_tree_dn => 'cn=users,cn=accounts,dc=example,dc=com', + user_id_attribute => 'uid', + user_name_attribute => 'uid', + user_mail_attribute => 'mail', + user_allow_create => 'False', + user_allow_update => 'False', + user_allow_delete => 'False', + user_enabled_emulation => 'True', + user_enabled_emulation_dn => 'cn=openstack-enabled,cn=groups,cn=accounts,dc=example,dc=com', + group_tree_dn => 'ou=groups,ou=openstack,dc=example,dc=com', + group_objectclass => 'organizationalRole', + group_id_attribute => 'cn', + group_name_attribute => 'cn', + group_member_attribute => 'RoleOccupant', + group_desc_attribute => 'description', + group_allow_create => 'True', + group_allow_update => 'True', + group_allow_delete => 'True', + tenant_tree_dn => 'ou=projects,ou=openstack,dc=example,dc=com', + tenant_objectclass => 'organizationalUnit', + tenant_id_attribute => 'ou', + tenant_member_attribute => 'member', + tenant_name_attribute => 'ou', + tenant_desc_attribute => 'description', + tenant_allow_create => 'True', + tenant_allow_update => 'True', + tenant_allow_delete => 'True', + tenant_enabled_emulation => 'True', + tenant_enabled_emulation_dn => 'cn=enabled,ou=openstack,dc=example,dc=com', + role_tree_dn => 'ou=roles,ou=openstack,dc=example,dc=com', + role_objectclass => 'organizationalRole', + role_id_attribute => 'cn', + role_name_attribute => 'cn', + role_member_attribute => 'roleOccupant', + role_allow_create => 'True', + role_allow_update => 'True', + role_allow_delete => 'True', + identity_driver => 'keystone.identity.backends.ldap.Identity', + assignment_driver => 'keystone.assignment.backends.ldap.Assignment', + use_tls => 'True', + tls_cacertfile => '/etc/ssl/certs/ca-certificates.crt', + tls_req_cert => 'demand', +} diff --git a/keystone/examples/ldap_identity.pp b/keystone/examples/ldap_identity.pp new file mode 100644 index 000000000..41272c52f --- /dev/null +++ b/keystone/examples/ldap_identity.pp @@ -0,0 +1,28 @@ +# Example using LDAP to manage user identity only. +# This setup will not allow changes to users. + +# Ensure this matches what is in LDAP or keystone will try to recreate +# the admin user +class { 'keystone::roles::admin': + email => 'test@example.com', + password => 'ChangeMe', +} + +# You can test this connection with ldapsearch first to ensure it works. +# This was tested against a FreeIPA box, you will likely need to change the +# attributes to match your configuration. +class { 'keystone:ldap': + identity_driver => 'keystone.identity.backends.ldap.Identity', + url => 'ldap://ldap.example.com:389', + user => 'uid=bind,cn=users,cn=accounts,dc=example,dc=com', + password => 'SecretPass', + suffix => 'dc=example,dc=com', + query_scope => 'sub', + user_tree_dn => 'cn=users,cn=accounts,dc=example,dc=com', + user_id_attribute => 'uid', + user_name_attribute => 'uid', + user_mail_attribute => 'mail', + user_allow_create => 'False', + user_allow_update => 'False', + user_allow_delete => 'False' +} diff --git a/keystone/ext/keystone_test.rb b/keystone/ext/keystone_test.rb new file mode 100644 index 000000000..ed944bedd --- /dev/null +++ b/keystone/ext/keystone_test.rb @@ -0,0 +1,55 @@ +#!/usr/bin/env ruby +# this script verifies that keystone has +# been successfully installed using the instructions +# found here: http://keystone.openstack.org/configuration.html + +begin + require 'rubygems' +rescue + puts 'Could not require rubygems. This assumes puppet is not installed as a gem' +end +require 'open3' +require 'fileutils' +require 'puppet' + +username='admin' +password='admin_password' +# required to get a real services catalog +tenant='openstack' + +# shared secret +service_token='service_token' + +def run_command(cmd) + Open3.popen3(cmd) do |stdin, stdout, stderr| + begin + stdout = stdout.read + puts "Response from token request:#{stdout}" + return stdout + rescue Exception => e + puts "Request failed, this sh*t is borked :( : details: #{e}" + exit 1 + end + end +end + +puts `puppet apply -e "package {curl: ensure => present }"` + +get_token = %(curl -d '{"auth":{"passwordCredentials":{"username": "#{username}", "password": "#{password}"}}}' -H "Content-type: application/json" http://localhost:35357/v2.0/tokens) +token = nil + +puts "Running auth command: #{get_token}" +token = PSON.load(run_command(get_token))["access"]["token"]["id"] + +if token + puts "We were able to retrieve a token" + puts token + verify_token = "curl -H 'X-Auth-Token: #{service_token}' http://localhost:35357/v2.0/tokens/#{token}" + puts 'verifying token' + run_command(verify_token) + ['endpoints', 'tenants', 'users'].each do |x| + puts "getting #{x}" + get_keystone_data = "curl -H 'X-Auth-Token: #{service_token}' http://localhost:35357/v2.0/#{x}" + run_command(get_keystone_data) + end +end diff --git a/keystone/files/httpd/keystone.py b/keystone/files/httpd/keystone.py new file mode 100644 index 000000000..a766e199f --- /dev/null +++ b/keystone/files/httpd/keystone.py @@ -0,0 +1,54 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# +# 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. + +# +# This file was copied from https://github.com/openstack/keystone/raw/c3b92295b718a41c3136876eb39297081015a97c/httpd/keystone.py +# It's only required for platforms on which it is not packaged yet. +# It should be removed when available everywhere in a package. +# + +import logging +import os + +from paste import deploy + +from keystone.openstack.common import gettextutils + +# NOTE(blk-u): +# gettextutils.install() must run to set _ before importing any modules that +# contain static translated strings. +gettextutils.install('keystone') + +from keystone.common import environment +from keystone import config +from keystone.openstack.common import log + + +CONF = config.CONF +CONF(project='keystone') +config.setup_logging(CONF) + +environment.use_stdlib() +name = os.path.basename(__file__) + +if CONF.debug: + CONF.log_opt_values(log.getLogger(CONF.prog), logging.DEBUG) + +# NOTE(ldbragst): 'application' is required in this context by WSGI spec. +# The following is a reference to Python Paste Deploy documentation +# http://pythonpaste.org/deploy/ +application = deploy.loadapp('config:%s' % config.find_paste_config(), + name=name) diff --git a/keystone/lib/puppet/provider/keystone.rb b/keystone/lib/puppet/provider/keystone.rb new file mode 100644 index 000000000..05bdc7c99 --- /dev/null +++ b/keystone/lib/puppet/provider/keystone.rb @@ -0,0 +1,190 @@ +require 'puppet/util/inifile' +class Puppet::Provider::Keystone < Puppet::Provider + + # retrieves the current token from keystone.conf + def self.admin_token + @admin_token ||= get_admin_token + end + + def self.get_admin_token + if keystone_file and keystone_file['DEFAULT'] and keystone_file['DEFAULT']['admin_token'] + return "#{keystone_file['DEFAULT']['admin_token'].strip}" + else + raise(Puppet::Error, "File: /etc/keystone/keystone.conf does not contain a section DEFAULT with the admin_token specified. Keystone types will not work if keystone is not correctly configured") + end + end + + def self.admin_endpoint + @admin_endpoint ||= get_admin_endpoint + end + + def self.get_admin_endpoint + admin_endpoint = keystone_file['DEFAULT']['admin_endpoint'] ? keystone_file['DEFAULT']['admin_endpoint'].strip.chomp('/') : nil + return "#{admin_endpoint}/v2.0/" if admin_endpoint + + admin_port = keystone_file['DEFAULT']['admin_port'] ? keystone_file['DEFAULT']['admin_port'].strip : '35357' + ssl = keystone_file['ssl'] && keystone_file['ssl']['enable'] ? keystone_file['ssl']['enable'].strip.downcase == 'true' : false + protocol = ssl ? 'https' : 'http' + if keystone_file and keystone_file['DEFAULT'] and keystone_file['DEFAULT']['admin_bind_host'] + host = keystone_file['DEFAULT']['admin_bind_host'].strip + if host == "0.0.0.0" + host = "127.0.0.1" + end + else + host = "127.0.0.1" + end + "#{protocol}://#{host}:#{admin_port}/v2.0/" + end + + def self.keystone_file + return @keystone_file if @keystone_file + @keystone_file = Puppet::Util::IniConfig::File.new + @keystone_file.read('/etc/keystone/keystone.conf') + @keystone_file + end + + def self.tenant_hash + @tenant_hash ||= build_tenant_hash + end + + def tenant_hash + self.class.tenant_hash + end + + def self.reset + @admin_endpoint = nil + @tenant_hash = nil + @admin_token = nil + @keystone_file = nil + end + + # the path to withenv changes between versions of puppet, so redefining this function here, + # Run some code with a specific environment. Resets the environment at the end of the code. + def self.withenv(hash, &block) + saved = ENV.to_hash + hash.each do |name, val| + ENV[name.to_s] = val + end + block.call + ensure + ENV.clear + saved.each do |name, val| + ENV[name] = val + end + end + + def self.auth_keystone(*args) + authenv = {:OS_SERVICE_TOKEN => admin_token} + begin + withenv authenv do + remove_warnings(keystone('--os-endpoint', admin_endpoint, args)) + end + rescue Exception => e + if e.message =~ /(\(HTTP\s+400\))|(\[Errno 111\]\s+Connection\s+refused)|(503\s+Service\s+Unavailable)|(Max\s+retries\s+exceeded)|(Unable\s+to\s+establish\s+connection)/ + sleep 10 + withenv authenv do + remove_warnings(keystone('--os-endpoint', admin_endpoint, args)) + end + else + raise(e) + end + end + end + + def auth_keystone(*args) + self.class.auth_keystone(args) + end + + def self.creds_keystone(name, tenant, password, *args) + authenv = {:OS_USERNAME => name, :OS_TENANT_NAME => tenant, :OS_PASSWORD => password} + begin + withenv authenv do + remove_warnings(keystone('--os-auth-url', admin_endpoint, args)) + end + rescue Exception => e + if e.message =~ /(\(HTTP\s+400\))|(\[Errno 111\]\s+Connection\s+refused)|(503\s+Service\s+Unavailable)|(Max\s+retries\s+exceeded)|(Unable\s+to\s+establish\s+connection)/ + sleep 10 + withenv authenv do + remove_warnings(keystone('--os-auth-url', admin_endpoint, args)) + end + else + raise(e) + end + end + end + + def creds_keystone(name, tenant, password, *args) + self.class.creds_keystone(name, tenant, password, args) + end + + def self.parse_keystone_object(data) + # Parse the output of [type]-{create,get} into a hash + attrs = {} + header_lines = 3 + footer_lines = 1 + data.split("\n")[header_lines...-footer_lines].each do |line| + if match_data = /\|\s([^|]+)\s\|\s([^|]+)\s\|/.match(line) + attrs[match_data[1].strip] = match_data[2].strip + end + end + attrs + end + + private + + def self.list_keystone_objects(type, number_columns, *args) + # this assumes that all returned objects are of the form + # id, name, enabled_state, OTHER + # number_columns can be a Fixnum or an Array of possible values that can be returned + list = (auth_keystone("#{type}-list", args).split("\n")[3..-2] || []).collect do |line| + row = line.split(/\|/)[1..-1] + row = row.map {|x| x.strip } + # if both checks fail then we have a mismatch between what was expected and what was received + if (number_columns.class == Array and !number_columns.include? row.size) or (number_columns.class == Fixnum and row.size != number_columns) + raise(Puppet::Error, "Expected #{number_columns} columns for #{type} row, found #{row.size}. Line #{line}") + end + row + end + list + end + + def self.get_keystone_object(type, id, attr) + id = id.chomp + auth_keystone("#{type}-get", id).split(/\|\n/m).each do |line| + if line =~ /\|(\s+)?#{attr}(\s+)?\|/ + if line.kind_of?(Array) + return line[0].split("|")[2].strip + else + return line.split("|")[2].strip + end + else + nil + end + end + raise(Puppet::Error, "Could not find colummn #{attr} when getting #{type} #{id}") + end + + # remove warning from the output. this is a temporary hack until + # I refactor things to use the the rest API + def self.remove_warnings(results) + found_header = false + in_warning = false + results.split("\n").collect do |line| + unless found_header + if line =~ /^\+[-\+]+\+$/ + in_warning = false + found_header = true + line + elsif line =~ /^WARNING/ or line =~ /UserWarning/ or in_warning + # warnings can be multi line, we have to skip all of them + in_warning = true + nil + else + line + end + else + line + end + end.compact.join("\n") + end +end diff --git a/keystone/lib/puppet/provider/keystone_config/ini_setting.rb b/keystone/lib/puppet/provider/keystone_config/ini_setting.rb new file mode 100644 index 000000000..4d7b5cbdb --- /dev/null +++ b/keystone/lib/puppet/provider/keystone_config/ini_setting.rb @@ -0,0 +1,27 @@ +Puppet::Type.type(:keystone_config).provide( + :ini_setting, + :parent => Puppet::Type.type(:ini_setting).provider(:ruby) +) do + + def section + resource[:name].split('/', 2).first + end + + def setting + resource[:name].split('/', 2).last + end + + def separator + '=' + end + + def self.file_path + '/etc/keystone/keystone.conf' + end + + # added for backwards compatibility with older versions of inifile + def file_path + self.class.file_path + end + +end diff --git a/keystone/lib/puppet/provider/keystone_endpoint/keystone.rb b/keystone/lib/puppet/provider/keystone_endpoint/keystone.rb new file mode 100644 index 000000000..c7696c8a6 --- /dev/null +++ b/keystone/lib/puppet/provider/keystone_endpoint/keystone.rb @@ -0,0 +1,125 @@ +$LOAD_PATH.push(File.join(File.dirname(__FILE__), '..', '..', '..')) +require 'puppet/provider/keystone' +Puppet::Type.type(:keystone_endpoint).provide( + :keystone, + :parent => Puppet::Provider::Keystone +) do + + desc <<-EOT + Provider that uses the keystone client tool to + manage keystone endpoints + + This provider makes a few assumptions/ + 1. assumes that the admin endpoint can be accessed via localhost. + 2. Assumes that the admin token and port can be accessed from + /etc/keystone/keystone.conf + EOT + + optional_commands :keystone => "keystone" + + def initialize(resource = nil) + super(resource) + @property_flush = {} + end + + def self.prefetch(resources) + endpoints = instances + resources.keys.each do |name| + if provider = endpoints.find{ |endpoint| endpoint.name == name } + resources[name].provider = provider + end + end + end + + def self.instances + list_keystone_objects('endpoint', [5,6]).collect do |endpoint| + service_name = get_keystone_object('service', endpoint[5], 'name') + new( + :name => "#{endpoint[1]}/#{service_name}", + :ensure => :present, + :id => endpoint[0], + :region => endpoint[1], + :public_url => endpoint[2], + :internal_url => endpoint[3], + :admin_url => endpoint[4], + :service_id => endpoint[5], + :service_name => service_name + ) + end + end + + def create + optional_opts = [] + { + :public_url => '--publicurl', + :internal_url => '--internalurl', + :admin_url => '--adminurl' + }.each do |param, opt| + if resource[param] + optional_opts.push(opt).push(resource[param]) + end + end + + (region, service_name) = resource[:name].split('/') + resource[:region] = region + optional_opts.push('--region').push(resource[:region]) + + service_id = self.class.list_keystone_objects('service', 4).detect do |s| + s[1] == service_name + end.first + + auth_keystone('endpoint-create', '--service-id', service_id, optional_opts) + end + + def exists? + @property_hash[:ensure] == :present + end + + def destroy + auth_keystone('endpoint-delete', @property_hash[:id]) + end + + def flush + if ! @property_flush.empty? + destroy + create + @property_flush.clear + end + @property_hash = resource.to_hash + end + + def id + @property_hash[:id] + end + + def region + @property_hash[:region] + end + + def public_url + @property_hash[:public_url] + end + + def internal_url + @property_hash[:internal_url] + end + + def admin_url + @property_hash[:admin_url] + end + + def public_url=(value) + @property_hash[:public_url] = value + @property_flush[:public_url] = value + end + + def internal_url=(value) + @property_hash[:internal_url] = value + @property_flush[:internal_url] = value + end + + def admin_url=(value) + @property_hash[:admin_url] = value + @property_flush[:admin_url] = value + end +end diff --git a/keystone/lib/puppet/provider/keystone_role/keystone.rb b/keystone/lib/puppet/provider/keystone_role/keystone.rb new file mode 100644 index 000000000..86b5f47b6 --- /dev/null +++ b/keystone/lib/puppet/provider/keystone_role/keystone.rb @@ -0,0 +1,65 @@ +$LOAD_PATH.push(File.join(File.dirname(__FILE__), '..', '..', '..')) +require 'puppet/provider/keystone' +Puppet::Type.type(:keystone_role).provide( + :keystone, + :parent => Puppet::Provider::Keystone +) do + + desc <<-EOT + Provider that uses the keystone client tool to + manage keystone roles + EOT + + optional_commands :keystone => "keystone" + + def self.prefetch(resource) + # rebuild the cahce for every puppet run + @role_hash = nil + end + + def self.role_hash + @role_hash ||= build_role_hash + end + + def role_hash + self.class.role_hash + end + + def self.instances + role_hash.collect do |k, v| + new(:name => k) + end + end + + def create + auth_keystone( + 'role-create', + '--name', resource[:name] + ) + end + + def exists? + role_hash[resource[:name]] + end + + def destroy + auth_keystone('role-delete', role_hash[resource[:name]][:id]) + end + + def id + role_hash[resource[:name]][:id] + end + + private + + def self.build_role_hash + hash = {} + list_keystone_objects('role', 2).each do |role| + hash[role[1]] = { + :id => role[0], + } + end + hash + end + +end diff --git a/keystone/lib/puppet/provider/keystone_service/keystone.rb b/keystone/lib/puppet/provider/keystone_service/keystone.rb new file mode 100644 index 000000000..00dbad875 --- /dev/null +++ b/keystone/lib/puppet/provider/keystone_service/keystone.rb @@ -0,0 +1,97 @@ +$LOAD_PATH.push(File.join(File.dirname(__FILE__), '..', '..', '..')) +require 'puppet/provider/keystone' +Puppet::Type.type(:keystone_service).provide( + :keystone, + :parent => Puppet::Provider::Keystone +) do + + desc <<-EOT + Provider that uses the keystone client tool to + manage keystone services + + This provider makes a few assumptions/ + 1. assumes that the admin endpoint can be accessed via localhost. + 2. Assumes that the admin token and port can be accessed from + /etc/keystone/keystone.conf + + Does not support the ability to list all + EOT + + optional_commands :keystone => "keystone" + + def self.prefetch(resource) + # rebuild the cahce for every puppet run + @service_hash = nil + end + + def self.service_hash + @service_hash ||= build_service_hash + end + + def service_hash + self.class.service_hash + end + + def self.instances + service_hash.collect do |k, v| + new(:name => k) + end + end + + def create + optional_opts = [] + raise(Puppet::Error, "Required property type not specified for KeystoneService[#{resource[:name]}]") unless resource[:type] + if resource[:description] + optional_opts.push('--description').push(resource[:description]) + end + auth_keystone( + 'service-create', + '--name', resource[:name], + '--type', resource[:type], + optional_opts + ) + end + + def exists? + service_hash[resource[:name]] + end + + def destroy + auth_keystone('service-delete', service_hash[resource[:name]][:id]) + end + + def id + service_hash[resource[:name]][:id] + end + + def type + service_hash[resource[:name]][:type] + end + + def type=(value) + raise(Puppet::Error, "service-update is not currently supported by the keystone sql driver") + end + + def description + service_hash[resource[:name]][:description] + end + + def description=(value) + raise(Puppet::Error, "service-update is not currently supported by the keystone sql driver") + end + + private + + def self.build_service_hash + hash = {} + list_keystone_objects('service', 4).each do |user| + hash[user[1]] = { + :id => user[0], + :type => user[2], + :description => user[3] + } + end + hash + end + +end diff --git a/keystone/lib/puppet/provider/keystone_tenant/keystone.rb b/keystone/lib/puppet/provider/keystone_tenant/keystone.rb new file mode 100644 index 000000000..1c107f232 --- /dev/null +++ b/keystone/lib/puppet/provider/keystone_tenant/keystone.rb @@ -0,0 +1,120 @@ +$LOAD_PATH.push(File.join(File.dirname(__FILE__), '..', '..', '..')) +require 'puppet/provider/keystone' +Puppet::Type.type(:keystone_tenant).provide( + :keystone, + :parent => Puppet::Provider::Keystone +) do + + desc <<-EOT + Provider that uses the keystone client tool to + manage keystone tenants + + This provider makes a few assumptions/ + 1. assumes that the admin endpoint can be accessed via localhost. + 2. Assumes that the admin token and port can be accessed from + /etc/keystone/keystone.conf + + One string difference, is that it does not know how to change the + name of a tenant + EOT + + optional_commands :keystone => "keystone" + + def self.prefetch(resource) + # rebuild the cahce for every puppet run + @tenant_hash = nil + end + + def self.tenant_hash + @tenant_hash ||= build_tenant_hash + end + + def tenant_hash + self.class.tenant_hash + end + + def instance + tenant_hash[resource[:name]] + end + + def self.instances + tenant_hash.collect do |k, v| + new( + :name => k, + :id => v[:id] + ) + end + end + + def create + optional_opts = [] + if resource[:description] + optional_opts.push('--description').push(resource[:description]) + end + results = auth_keystone( + 'tenant-create', + '--name', resource[:name], + '--enabled', resource[:enabled], + optional_opts + ) + + if results =~ /Property\s|\sValue/ + attrs = self.class.parse_keystone_object(results) + tenant_hash[resource[:name]] = { + :ensure => :present, + :name => resource[:name], + :id => attrs['id'], + :enabled => attrs['enabled'], + :description => attrs['description'], + } + else + fail("did not get expected message on tenant creation, got #{results}") + end + end + + def exists? + instance + end + + def destroy + auth_keystone('tenant-delete', instance[:id]) + instance[:ensure] = :absent + end + + def enabled=(value) + Puppet.warning("I am not sure if this is supported yet") + auth_keystone("tenant-update", '--enabled', value, instance[:id]) + instance[:enabled] = value + end + + def description + self.class.get_keystone_object('tenant', instance[:id], 'description') + end + + def description=(value) + auth_keystone("tenant-update", '--description', value, instance[:id]) + instance[:description] = value + end + + [ + :id, + :enabled, + ].each do |attr| + define_method(attr.to_s) do + instance[attr] || :absent + end + end + + private + + def self.build_tenant_hash + hash = {} + list_keystone_objects('tenant', 3).each do |tenant| + hash[tenant[1]] = { + :id => tenant[0], + :enabled => tenant[2], + } + end + hash + end +end diff --git a/keystone/lib/puppet/provider/keystone_user/keystone.rb b/keystone/lib/puppet/provider/keystone_user/keystone.rb new file mode 100644 index 000000000..a2da28ff2 --- /dev/null +++ b/keystone/lib/puppet/provider/keystone_user/keystone.rb @@ -0,0 +1,159 @@ +$LOAD_PATH.push(File.join(File.dirname(__FILE__), '..', '..', '..')) +require 'puppet/provider/keystone' +Puppet::Type.type(:keystone_user).provide( + :keystone, + :parent => Puppet::Provider::Keystone +) do + + desc <<-EOT + Provider that uses the keystone client tool to + manage keystone users + + This provider makes a few assumptions/ + 1. assumes that the admin endpoint can be accessed via localhost. + 2. Assumes that the admin token and port can be accessed from + /etc/keystone/keystone.conf + + Does not support the ability to update the user's name + EOT + + optional_commands :keystone => "keystone" + + def self.prefetch(resource) + # rebuild the cahce for every puppet run + @user_hash = nil + end + + def self.user_hash + @user_hash ||= build_user_hash + end + + def user_hash + self.class.user_hash + end + + def self.instances + user_hash.collect do |k, v| + new(:name => k) + end + end + + def create + optional_opts = [] + if resource[:email] + optional_opts.push('--email').push(resource[:email]) + end + if resource[:password] + optional_opts.push('--pass').push(resource[:password]) + end + if resource[:tenant] + tenant_id = self.class.list_keystone_objects('tenant', 3).collect {|x| + x[0] if x[1] == resource[:tenant] + }.compact[0] + optional_opts.push('--tenant_id').push(tenant_id) + end + auth_keystone( + 'user-create', + '--name', resource[:name], + '--enabled', resource[:enabled], + optional_opts + ) + end + + def exists? + user_hash[resource[:name]] + end + + def destroy + auth_keystone('user-delete', user_hash[resource[:name]][:id]) + end + + def enabled + user_hash[resource[:name]][:enabled] + end + + def enabled=(value) + auth_keystone( + "user-update", + '--enabled', value, + user_hash[resource[:name]][:id] + ) + end + + def password + # if we don't know a password we can't test it + return nil if resource[:password] == nil + # we can't get the value of the password but we can test to see if the one we know + # about works, if it doesn't then return nil, causing it to be reset + begin + token_out = creds_keystone(resource[:name], resource[:tenant], resource[:password], "token-get") + rescue Exception => e + return nil if e.message =~ /Not Authorized/ or e.message =~ /HTTP 401/ + raise e + end + return resource[:password] + end + + def password=(value) + auth_keystone('user-password-update', '--pass', value, user_hash[resource[:name]][:id]) + end + + def tenant + return resource[:tenant] if resource[:ignore_default_tenant] + user_id = user_hash[resource[:name]][:id] + begin + tenantId = self.class.get_keystone_object('user', user_id, 'tenantId') + rescue + tenantId = nil + end + if tenantId.nil? or tenantId == 'None' or tenantId.empty? + tenant = 'None' + else + # this prevents is from failing if tenant no longer exists + begin + tenant = self.class.get_keystone_object('tenant', tenantId, 'name') + rescue + tenant = 'None' + end + end + tenant + end + + def tenant=(value) + fail("tenant cannot be updated. Transition requested: #{user_hash[resource[:name]][:tenant]} -> #{value}") + end + + def email + user_hash[resource[:name]][:email] + end + + def email=(value) + auth_keystone( + "user-update", + '--email', value, + user_hash[resource[:name]][:id] + ) + end + + def id + user_hash[resource[:name]][:id] + end + + private + + def self.build_user_hash + hash = {} + list_keystone_objects('user', 4).each do |user| + password = 'nil' + hash[user[1]] = { + :id => user[0], + :enabled => user[2], + :email => user[3], + :name => user[1], + :password => password, + } + end + hash + end + +end diff --git a/keystone/lib/puppet/provider/keystone_user_role/keystone.rb b/keystone/lib/puppet/provider/keystone_user_role/keystone.rb new file mode 100644 index 000000000..0b2d81b05 --- /dev/null +++ b/keystone/lib/puppet/provider/keystone_user_role/keystone.rb @@ -0,0 +1,230 @@ +$LOAD_PATH.push(File.join(File.dirname(__FILE__), '..', '..', '..')) +require 'puppet/provider/keystone' +Puppet::Type.type(:keystone_user_role).provide( + :keystone, + :parent => Puppet::Provider::Keystone +) do + + desc <<-EOT + Provider that uses the keystone client tool to + manage keystone role assignments to users + EOT + + optional_commands :keystone => "keystone" + + + def self.prefetch(resource) + # rebuild the cahce for every puppet run + @user_role_hash = nil + end + + def self.user_role_hash + @user_role_hash ||= build_user_role_hash + end + + def user_role_hash + self.class.user_role_hash + end + + def self.instances + user_role_hash.collect do |k, v| + new(:name => k) + end + end + + def create + user_id, tenant_id = get_user_and_tenant + resource[:roles].each do |role_name| + role_id = self.class.get_role(role_name) + auth_keystone( + 'user-role-add', + '--user-id', user_id, + '--tenant-id', tenant_id, + '--role-id', role_id + ) + end + end + + def self.get_user_and_tenant(user, tenant) + @tenant_hash ||= {} + @user_hash ||= {} + @tenant_hash[tenant] = @tenant_hash[tenant] || get_tenant(tenant) + [ + get_user(@tenant_hash[tenant], user), + @tenant_hash[tenant] + ] + end + + def get_user_and_tenant + user = resource[:name].rpartition('@').first + tenant = resource[:name].rpartition('@').last + self.class.get_user_and_tenant(user, tenant) + end + + def exists? + user_id, tenant_id = get_user_and_tenant + get_user_tenant_hash(user_id, tenant_id) + end + + def destroy + user_id, tenant_id = get_user_and_tenant + get_user_tenant_hash(user_id, tenant_id)[:role_ids].each do |role_id| + auth_keystone( + 'user-role-remove', + '--user-id', user_id, + '--tenant-id', tenant_id, + '--role-id', role_id + ) + end + end + + def id + user_id, tenant_id = get_user_and_tenant + get_user_tenant_hash(user_id, tenant_id)[:id] + end + + def roles + user_id, tenant_id = get_user_and_tenant + get_user_tenant_hash(user_id, tenant_id)[:role_names] + end + + def roles=(value) + # determine the roles to be added and removed + remove = roles - Array(value) + add = Array(value) - roles + + user_id, tenant_id = get_user_and_tenant + + add.each do |role_name| + role_id = self.class.get_role(role_name) + auth_keystone( + 'user-role-add', + '--user-id', user_id, + '--tenant-id', tenant_id, + '--role-id', role_id + ) + end + remove.each do |role_name| + role_id = self.class.get_role(role_name) + auth_keystone( + 'user-role-remove', + '--user-id', user_id, + '--tenant-id', tenant_id, + '--role-id', role_id + ) + end + + end + + private + + def self.build_user_role_hash + hash = {} + get_tenants.each do |tenant_name, tenant_id| + get_users(tenant_id).each do |user_name, user_id| + list_user_roles(user_id, tenant_id).sort.each do |role| + hash["#{user_name}@#{tenant_name}"] ||= { + :user_id => user_id, + :tenant_id => tenant_id, + :role_names => [], + :role_ids => [] + } + hash["#{user_name}@#{tenant_name}"][:role_names].push(role[1]) + hash["#{user_name}@#{tenant_name}"][:role_ids].push(role[0]) + end + end + end + hash + end + + # lookup the roles for a single tenant/user combination + def get_user_tenant_hash(user_id, tenant_id) + @user_tenant_hash ||= {} + unless @user_tenant_hash["#{user_id}@#{tenant_id}"] + list_user_roles(user_id, tenant_id).sort.each do |role| + @user_tenant_hash["#{user_id}@#{tenant_id}"] ||= { + :user_id => user_id, + :tenant_id => tenant_id, + :role_names => [], + :role_ids => [] + } + @user_tenant_hash["#{user_id}@#{tenant_id}"][:role_names].push(role[1]) + @user_tenant_hash["#{user_id}@#{tenant_id}"][:role_ids].push(role[0]) + end + end + @user_tenant_hash["#{user_id}@#{tenant_id}"] + end + + + def self.list_user_roles(user_id, tenant_id) + # this assumes that all returned objects are of the form + # id, name, enabled_state, OTHER + number_columns = 4 + role_output = auth_keystone('user-role-list', '--user-id', user_id, '--tenant-id', tenant_id) + list = (role_output.split("\n")[3..-2] || []).collect do |line| + row = line.split(/\s*\|\s*/)[1..-1] + if row.size != number_columns + raise(Puppet::Error, "Expected #{number_columns} columns for #{type} row, found #{row.size}. Line #{line}") + end + row + end + list + end + + def list_user_roles(user_id, tenant_id) + self.class.list_user_roles(user_id, tenant_id) + end + + def self.get_user(tenant_id, name) + @users ||= {} + user_key = "#{name}@#{tenant_id}" + unless @users[user_key] + list_keystone_objects('user', 4, '--tenant-id', tenant_id).each do |user| + @users["#{user[1]}@#{tenant_id}"] = user[0] + end + end + @users[user_key] + end + + def self.get_users(tenant_id='') + @users = {} + list_keystone_objects('user', 4, '--tenant-id', tenant_id).each do |user| + @users[user[1]] = user[0] + end + @users + end + + def self.get_tenants + unless @tenants + @tenants = {} + list_keystone_objects('tenant', 3).each do |tenant| + @tenants[tenant[1]] = tenant[0] + end + end + @tenants + end + + def self.get_tenant(name) + unless (@tenants and @tenants[name]) + @tenants = {} + list_keystone_objects('tenant', 3).each do |tenant| + if tenant[1] == name + @tenants[tenant[1]] = tenant[0] + #tenant + end + end + end + @tenants[name] + end + + def self.get_role(name) + @roles ||= {} + unless @roles[name] + list_keystone_objects('role', 2).each do |role| + @roles[role[1]] = role[0] + end + end + @roles[name] + end + +end diff --git a/keystone/lib/puppet/type/keystone_config.rb b/keystone/lib/puppet/type/keystone_config.rb new file mode 100644 index 000000000..fc6b82040 --- /dev/null +++ b/keystone/lib/puppet/type/keystone_config.rb @@ -0,0 +1,44 @@ +Puppet::Type.newtype(:keystone_config) do + + ensurable + + newparam(:name, :namevar => true) do + desc 'Section/setting name to manage from keystone.conf' + newvalues(/\S+\/\S+/) + end + + newproperty(:value) do + desc 'The value of the setting to be defined.' + munge do |value| + value = value.to_s.strip + value.capitalize! if value =~ /^(true|false)$/i + value + end + newvalues(/^[\S ]*$/) + + def is_to_s( currentvalue ) + if resource.secret? + return '[old secret redacted]' + else + return currentvalue + end + end + + def should_to_s( newvalue ) + if resource.secret? + return '[new secret redacted]' + else + return newvalue + end + end + end + + newparam(:secret, :boolean => true) do + desc 'Whether to hide the value from Puppet logs. Defaults to `false`.' + + newvalues(:true, :false) + + defaultto false + end + +end diff --git a/keystone/lib/puppet/type/keystone_endpoint.rb b/keystone/lib/puppet/type/keystone_endpoint.rb new file mode 100644 index 000000000..3b461b5bc --- /dev/null +++ b/keystone/lib/puppet/type/keystone_endpoint.rb @@ -0,0 +1,43 @@ +Puppet::Type.newtype(:keystone_endpoint) do + + desc <<-EOT + This is currently used to model the management of + keystone endpoint. + EOT + + ensurable + + newparam(:name, :namevar => true) do + newvalues(/\S+\/\S+/) + end + + newproperty(:id) do + validate do |v| + raise(Puppet::Error, 'This is a read only property') + end + end + + newproperty(:region) do + end + + # TODO I should do some url validation + newproperty(:public_url) do + end + + newproperty(:internal_url) do + end + + newproperty(:admin_url) do + end + + # we should not do anything until the keystone service is started + autorequire(:service) do + ['keystone'] + end + + autorequire(:keystone_service) do + (region, service_name) = self[:name].split('/') + [service_name] + end + +end diff --git a/keystone/lib/puppet/type/keystone_role.rb b/keystone/lib/puppet/type/keystone_role.rb new file mode 100644 index 000000000..7893250ba --- /dev/null +++ b/keystone/lib/puppet/type/keystone_role.rb @@ -0,0 +1,25 @@ +Puppet::Type.newtype(:keystone_role) do + + desc <<-EOT + This is currently used to model the creation of + keystone roles. + EOT + + ensurable + + newparam(:name, :namevar => true) do + newvalues(/\S+/) + end + + newproperty(:id) do + validate do |v| + raise(Puppet::Error, 'This is a read only property') + end + end + + # we should not do anything until the keystone service is started + autorequire(:service) do + ['keystone'] + end + +end diff --git a/keystone/lib/puppet/type/keystone_service.rb b/keystone/lib/puppet/type/keystone_service.rb new file mode 100644 index 000000000..124deba1b --- /dev/null +++ b/keystone/lib/puppet/type/keystone_service.rb @@ -0,0 +1,31 @@ +Puppet::Type.newtype(:keystone_service) do + + desc <<-EOT + This is currently used to model the management of + keystone services. + EOT + + ensurable + + newparam(:name, :namevar => true) do + newvalues(/\S+/) + end + + newproperty(:id) do + validate do |v| + raise(Puppet::Error, 'This is a read only property') + end + end + + newproperty(:type) do + end + + newproperty(:description) do + end + + # we should not do anything until the keystone service is started + autorequire(:service) do + ['keystone'] + end + +end diff --git a/keystone/lib/puppet/type/keystone_tenant.rb b/keystone/lib/puppet/type/keystone_tenant.rb new file mode 100644 index 000000000..513430878 --- /dev/null +++ b/keystone/lib/puppet/type/keystone_tenant.rb @@ -0,0 +1,38 @@ +Puppet::Type.newtype(:keystone_tenant) do + + desc <<-EOT + This type can be used to manage + keystone tenants. + + This is assumed to be running on the same node + as your keystone API server. + EOT + + ensurable + + newparam(:name, :namevar => true) do + newvalues(/\w+/) + end + + newproperty(:enabled) do + newvalues(/(t|T)rue/, /(f|F)alse/) + defaultto('True') + munge do |value| + value.to_s.capitalize + end + end + + newproperty(:description) + + newproperty(:id) do + validate do |v| + raise(Puppet::Error, 'This is a read only property') + end + end + + # we should not do anything until the keystone service is started + autorequire(:service) do + ['keystone'] + end + +end diff --git a/keystone/lib/puppet/type/keystone_user.rb b/keystone/lib/puppet/type/keystone_user.rb new file mode 100644 index 000000000..cc7e91716 --- /dev/null +++ b/keystone/lib/puppet/type/keystone_user.rb @@ -0,0 +1,74 @@ +Puppet::Type.newtype(:keystone_user) do + + desc <<-EOT + This is currently used to model the creation of + keystone users. + + It currently requires that both the password + as well as the tenant are specified. + EOT + +# TODO support description?? + + ensurable + + newparam(:name, :namevar => true) do + newvalues(/\S+/) + end + + newparam(:ignore_default_tenant, :boolean => true) do + newvalues(:true, :false) + defaultto false + end + + newproperty(:enabled) do + newvalues(/(t|T)rue/, /(f|F)alse/) + defaultto('True') + munge do |value| + value.to_s.capitalize + end + end + + newproperty(:password) do + newvalues(/\S+/) + def change_to_s(currentvalue, newvalue) + if currentvalue == :absent + return "created password" + else + return "changed password" + end + end + + def is_to_s( currentvalue ) + return '[old password redacted]' + end + + def should_to_s( newvalue ) + return '[new password redacted]' + end + end + + newproperty(:tenant) do + newvalues(/\S+/) + end + + newproperty(:email) do + newvalues(/^(\S+@\S+)|$/) + end + + newproperty(:id) do + validate do |v| + raise(Puppet::Error, 'This is a read only property') + end + end + + autorequire(:keystone_tenant) do + self[:tenant] + end + + # we should not do anything until the keystone service is started + autorequire(:service) do + ['keystone'] + end + +end diff --git a/keystone/lib/puppet/type/keystone_user_role.rb b/keystone/lib/puppet/type/keystone_user_role.rb new file mode 100644 index 000000000..60715fb8f --- /dev/null +++ b/keystone/lib/puppet/type/keystone_user_role.rb @@ -0,0 +1,51 @@ +Puppet::Type.newtype(:keystone_user_role) do + + desc <<-EOT + This is currently used to model the creation of + keystone users roles. + + User roles are an assignment of a role to a user on + a certain tenant. The combination of all of these + attributes is unique. + EOT + + ensurable + + newparam(:name, :namevar => true) do + newvalues(/^\S+@\S+$/) + #munge do |value| + # matchdata = /(\S+)@(\S+)/.match(value) + # { + # :user => matchdata[1], + # :tenant => matchdata[2] + # } + #nd + end + + newproperty(:roles, :array_matching => :all) do + end + + newproperty(:id) do + validate do |v| + raise(Puppet::Error, 'This is a read only property') + end + end + + autorequire(:keystone_user) do + self[:name].rpartition('@').first + end + + autorequire(:keystone_tenant) do + self[:name].rpartition('@').last + end + + autorequire(:keystone_role) do + self[:roles] + end + + # we should not do anything until the keystone service is started + autorequire(:service) do + ['keystone'] + end + +end diff --git a/keystone/manifests/client.pp b/keystone/manifests/client.pp new file mode 100644 index 000000000..453503d66 --- /dev/null +++ b/keystone/manifests/client.pp @@ -0,0 +1,17 @@ +# == Class: keystone::client +# +# Installs Keystone client. +# +# === Parameters +# +# [*ensure*] +# (optional) Ensure state of the package. Defaults to 'present'. +# +class keystone::client ( + $ensure = 'present' +) { + + package { 'python-keystoneclient': + ensure => $ensure, + } +} diff --git a/keystone/manifests/config.pp b/keystone/manifests/config.pp new file mode 100644 index 000000000..5309fa711 --- /dev/null +++ b/keystone/manifests/config.pp @@ -0,0 +1,30 @@ +# == Class: keystone::config +# +# This class is used to manage arbitrary keystone configurations. +# +# === Parameters +# +# [*keystone_config*] +# (optional) Allow configuration of arbitrary keystone configurations. +# The value is an hash of keystone_config resources. Example: +# { 'DEFAULT/foo' => { value => 'fooValue'}, +# 'DEFAULT/bar' => { value => 'barValue'} +# } +# In yaml format, Example: +# keystone_config: +# DEFAULT/foo: +# value: fooValue +# DEFAULT/bar: +# value: barValue +# +# NOTE: The configuration MUST NOT be already handled by this module +# or Puppet catalog compilation will fail with duplicate resources. +# +class keystone::config ( + $keystone_config = {}, +) { + + validate_hash($keystone_config) + + create_resources('keystone_config', $keystone_config) +} diff --git a/keystone/manifests/cron/token_flush.pp b/keystone/manifests/cron/token_flush.pp new file mode 100644 index 000000000..7b334b2a6 --- /dev/null +++ b/keystone/manifests/cron/token_flush.pp @@ -0,0 +1,57 @@ +# +# Copyright (C) 2014 eNovance SAS +# +# Author: Emilien Macchi +# +# 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: keystone::cron::token_flush +# +# Installs a cron job to purge expired tokens. +# +# === Parameters +# +# [*minute*] +# (optional) Defaults to '1'. +# +# [*hour*] +# (optional) Defaults to '0'. +# +# [*monthday*] +# (optional) Defaults to '*'. +# +# [*month*] +# (optional) Defaults to '*'. +# +# [*weekday*] +# (optional) Defaults to '*'. +# +class keystone::cron::token_flush ( + $minute = 1, + $hour = 0, + $monthday = '*', + $month = '*', + $weekday = '*', +) { + + cron { 'keystone-manage token_flush': + command => 'keystone-manage token_flush >>/var/log/keystone/keystone-tokenflush.log 2>&1', + environment => 'PATH=/bin:/usr/bin:/usr/sbin', + user => 'keystone', + minute => $minute, + hour => $hour, + monthday => $monthday, + month => $month, + weekday => $weekday + } +} diff --git a/keystone/manifests/db/mysql.pp b/keystone/manifests/db/mysql.pp new file mode 100644 index 000000000..3e046f4a2 --- /dev/null +++ b/keystone/manifests/db/mysql.pp @@ -0,0 +1,62 @@ +# The keystone::db::mysql class implements mysql backend for keystone +# +# This class can be used to create tables, users and grant +# privelege for a mysql keystone database. +# +# == parameters +# +# [password] Password that will be used for the keystone db user. +# Optional. Defaults to: 'keystone_default_password' +# +# [dbname] Name of keystone database. Optional. Defaults to keystone. +# +# [user] Name of keystone user. Optional. Defaults to keystone. +# +# [host] Host where user should be allowed all priveleges for database. +# Optional. Defaults to 127.0.0.1. +# +# [allowed_hosts] Hosts allowed to use the database +# +# [*mysql_module*] Deprecated. Does nothing. +# +# == Dependencies +# Class['mysql::server'] +# +# == Examples +# == Authors +# +# Dan Bode dan@puppetlabs.com +# +# == Copyright +# +# Copyright 2012 Puppetlabs Inc, unless otherwise noted. +# +class keystone::db::mysql( + $password, + $dbname = 'keystone', + $user = 'keystone', + $host = '127.0.0.1', + $charset = 'utf8', + $collate = 'utf8_unicode_ci', + $mysql_module = undef, + $allowed_hosts = undef +) { + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + validate_string($password) + + ::openstacklib::db::mysql { 'keystone': + user => $user, + password_hash => mysql_password($password), + dbname => $dbname, + host => $host, + charset => $charset, + collate => $collate, + allowed_hosts => $allowed_hosts, + } + + ::Openstacklib::Db::Mysql['keystone'] ~> Exec<| title == 'keystone-manage db_sync' |> +} diff --git a/keystone/manifests/db/postgresql.pp b/keystone/manifests/db/postgresql.pp new file mode 100644 index 000000000..3d7eb73d7 --- /dev/null +++ b/keystone/manifests/db/postgresql.pp @@ -0,0 +1,47 @@ +# +# implements postgresql backend for keystone +# +# This class can be used to create tables, users and grant +# privelege for a postgresql keystone database. +# +# Requires Puppetlabs Postgresql module. +# +# [*Parameters*] +# +# [password] Password that will be used for the keystone db user. +# Optional. Defaults to: 'keystone_default_password' +# +# [dbname] Name of keystone database. Optional. Defaults to keystone. +# +# [user] Name of keystone user. Optional. Defaults to keystone. +# +# == Dependencies +# Class['postgresql::server'] +# +# == Examples +# == Authors +# +# Etienne Pelletier epelletier@morphlabs.com +# +# == Copyright +# +# Copyright 2012 Etienne Pelletier, unless otherwise noted. +# +class keystone::db::postgresql( + $password, + $dbname = 'keystone', + $user = 'keystone' +) { + + Class['keystone::db::postgresql'] -> Service<| title == 'keystone' |> + + require postgresql::python + + postgresql::db { $dbname: + user => $user, + password => $password, + } + + Postgresql::Db[$dbname] ~> Exec<| title == 'keystone-manage db_sync' |> + +} diff --git a/keystone/manifests/db/sync.pp b/keystone/manifests/db/sync.pp new file mode 100644 index 000000000..5984a03a1 --- /dev/null +++ b/keystone/manifests/db/sync.pp @@ -0,0 +1,14 @@ +# +# Class to execute "keystone-manage db_sync +# +class keystone::db::sync { + exec { 'keystone-manage db_sync': + path => '/usr/bin', + user => 'keystone', + refreshonly => true, + subscribe => [Package['keystone'], Keystone_config['database/connection']], + require => User['keystone'], + } + + Exec['keystone-manage db_sync'] ~> Service<| title == 'keystone' |> +} diff --git a/keystone/manifests/dev/install.pp b/keystone/manifests/dev/install.pp new file mode 100644 index 000000000..3e68113c2 --- /dev/null +++ b/keystone/manifests/dev/install.pp @@ -0,0 +1,64 @@ +# +# Installs keystone from source. This is not yet fully implemented +# +# == Dependencies +# == Examples +# == Authors +# +# Dan Bode dan@puppetlabs.com +# +# == Copyright +# +# Copyright 2012 Puppetlabs Inc, unless otherwise noted. +# +class keystone::dev::install( + $source_dir = '/usr/local/keystone' +) { + # make sure that I have python 2.7 installed + + Class['openstack::dev'] -> Class['keystone::dev::install'] + + # there are likely conficts with other packages + # introduced by these resources + package { [ + 'python-dev', + 'libxml2-dev', + 'libxslt1-dev', + 'libsasl2-dev', + 'libsqlite3-dev', + 'libssl-dev', + 'libldap2-dev', + 'sqlite3' + ]: + ensure => latest, + } + + vcsrepo { $source_dir: + ensure => present, + provider => git, + source => 'git://github.com/openstack/keystone.git', + } + + Exec { + cwd => $source_dir, + path => '/usr/bin', + refreshonly => true, + subscribe => Vcsrepo[$source_dir], + logoutput => true, + # I have disabled timeout since this seems to take forever + # this may be a bad idea :) + timeout => 0, + } + + # TODO - really, I need a way to take this file and + # convert it into package resources + exec { 'install_dev_deps': + command => 'pip install -r tools/pip-requires', + } + + exec { 'install_keystone_source': + command => 'python setup.py develop', + require => Exec['install_dev_deps'], + } + +} diff --git a/keystone/manifests/endpoint.pp b/keystone/manifests/endpoint.pp new file mode 100644 index 000000000..96f2ec159 --- /dev/null +++ b/keystone/manifests/endpoint.pp @@ -0,0 +1,169 @@ +# == Class: keystone::endpoint +# +# Creates the auth endpoints for keystone +# +# === Parameters +# +# [*public_url*] +# (optional) Public url for keystone endpoint. (Defaults to 'http://127.0.0.1:5000') +# +# [*internal_url*] +# (optional) Internal url for keystone endpoint. (Defaults to $public_url) +# +# [*admin_url*] +# (optional) Admin url for keystone endpoint. (Defaults to 'http://127.0.0.1:35357') +# +# [*region*] +# (optional) Region for endpoint. (Defaults to 'RegionOne') +# +# [*version*] +# (optional) API version for endpoint. Appended to all endpoint urls. (Defaults to 'v2.0') +# +# [*public_url*] +# (optional) The endpoint's public url. (Defaults to 'http://127.0.0.1:5000') +# This url should *not* contain any version or trailing '/'. +# +# [*admin_url*] +# (optional) The endpoint's admin url. (Defaults to 'http://127.0.0.1:5000') +# This url should *not* contain any version or trailing '/'. +# +# [*internal_url*] +# (optional) The endpoint's internal url. (Defaults to 'http://127.0.0.1:35357') +# This url should *not* contain any version or trailing '/'. +# +# [*public_protocol*] +# (optional) DEPRECATED: Use public_url instead. +# Protocol for public access to keystone endpoint. (Defaults to 'http') +# Setting this parameter overrides public_url parameter. +# +# [*public_address*] +# (optional) DEPRECATED: Use public_url instead. +# Public address for keystone endpoint. (Defaults to '127.0.0.1') +# Setting this parameter overrides public_url parameter. +# +# [*public_port*] +# (optional) DEPRECATED: Use public_url instead. +# Port for non-admin access to keystone endpoint. (Defaults to 5000) +# Setting this parameter overrides public_url parameter. +# +# [*internal_address*] +# (optional) DEPRECATED: Use internal_url instead. +# Internal address for keystone endpoint. (Defaults to '127.0.0.1') +# Setting this parameter overrides internal_url parameter. +# +# [*internal_port*] +# (optional) DEPRECATED: Use internal_url instead. +# Port for internal access to keystone endpoint. (Defaults to $public_port) +# Setting this parameter overrides internal_url parameter. +# +# [*admin_address*] +# (optional) DEPRECATED: Use admin_url instead. +# Admin address for keystone endpoint. (Defaults to '127.0.0.1') +# Setting this parameter overrides admin_url parameter. +# +# [*admin_port*] +# (optional) DEPRECATED: Use admin_url instead. +# Port for admin access to keystone endpoint. (Defaults to 35357) +# Setting this parameter overrides admin_url parameter. +# +# === Deprecation notes +# +# If any value is provided for public_protocol, public_address or public_port parameters, +# public_url will be completely ignored. The same applies for internal and admin parameters. +# +# === Examples +# +# class { 'keystone::endpoint': +# public_url => 'https://154.10.10.23:5000', +# internal_url => 'https://11.0.1.7:5000', +# admin_url => 'https://10.0.1.7:35357', +# } +# +class keystone::endpoint ( + $public_url = 'http://127.0.0.1:5000', + $internal_url = undef, + $admin_url = 'http://127.0.0.1:35357', + $version = 'v2.0', + $region = 'RegionOne', + # DEPRECATED PARAMETERS + $public_protocol = undef, + $public_address = undef, + $public_port = undef, + $internal_address = undef, + $internal_port = undef, + $admin_address = undef, + $admin_port = undef, +) { + + if $public_port { + warning('The public_port parameter is deprecated, use public_url instead.') + } + + if $public_protocol { + warning('The public_protocol parameter is deprecated, use public_url instead.') + } + + if $public_address { + warning('The public_address parameter is deprecated, use public_url instead.') + } + + if $internal_address { + warning('The internal_address parameter is deprecated, use internal_url instead.') + } + + if $internal_port { + warning('The internal_port parameter is deprecated, use internal_url instead.') + } + + if $admin_address { + warning('The admin_address parameter is deprecated, use admin_url instead.') + } + + if $admin_port { + warning('The admin_port parameter is deprecated, use admin_url instead.') + } + + $public_url_real = inline_template('<%= + if (!@public_protocol.nil?) || (!@public_address.nil?) || (!@public_port.nil?) + @public_protocol ||= "http" + @public_address ||= "127.0.0.1" + @public_port ||= "5000" + "#{@public_protocol}://#{@public_address}:#{@public_port}/#{@version}" + else + "#{@public_url}/#{@version}" + end %>') + + $internal_url_real = inline_template('<%= + if (!@internal_address.nil?) || (!@internal_port.nil?) || (!@public_port.nil?) + @internal_address ||= @public_address ||= "127.0.0.1" + @internal_port ||= @public_port ||= "5000" + "http://#{@internal_address}:#{@internal_port}/#{@version}" + elsif (!@internal_url.nil?) + "#{@internal_url}/#{@version}" + else + "#{@public_url}/#{@version}" + end %>') + + $admin_url_real = inline_template('<%= + if (!@admin_address.nil?) || (!@admin_port.nil?) + @admin_address ||= "127.0.0.1" + @admin_port ||= "35357" + "http://#{@admin_address}:#{@admin_port}/#{@version}" + else + "#{@admin_url}/#{@version}" + end %>') + + keystone_service { 'keystone': + ensure => present, + type => 'identity', + description => 'OpenStack Identity Service', + } + + keystone_endpoint { "${region}/keystone": + ensure => present, + public_url => $public_url_real, + admin_url => $admin_url_real, + internal_url => $internal_url_real, + region => $region, + } +} diff --git a/keystone/manifests/init.pp b/keystone/manifests/init.pp new file mode 100644 index 000000000..55c88f2f0 --- /dev/null +++ b/keystone/manifests/init.pp @@ -0,0 +1,676 @@ +# +# Module for managing keystone config. +# +# == Parameters +# +# [package_ensure] Desired ensure state of packages. Optional. Defaults to present. +# accepts latest or specific versions. +# [bind_host] Host that keystone binds to. +# [bind_port] Port that keystone binds to. +# [public_port] +# [compute_port] +# [admin_port] +# [admin_port] Port that can be used for admin tasks. +# [admin_token] Admin token that can be used to authenticate as a keystone +# admin. Required. +# [verbose] Rather keystone should log at verbose level. Optional. +# Defaults to False. +# [debug] Rather keystone should log at debug level. Optional. +# Defaults to False. +# [use_syslog] Use syslog for logging. Optional. +# Defaults to False. +# [log_facility] Syslog facility to receive log lines. Optional. +# [catalog_type] Type of catalog that keystone uses to store endpoints,services. Optional. +# Defaults to sql. (Also accepts template) +# [catalog_driver] Catalog driver used by Keystone to store endpoints and services. Optional. +# Setting this value will override and ignore catalog_type. +# [catalog_template_file] Path to the catalog used if catalog_type equals 'template'. +# Defaults to '/etc/keystone/default_catalog.templates' +# [token_provider] Format keystone uses for tokens. Optional. +# Defaults to 'keystone.token.providers.uuid.Provider' +# Supports PKI and UUID. +# [token_driver] Driver to use for managing tokens. +# Optional. Defaults to 'keystone.token.backends.sql.Token' +# [token_expiration] Amount of time a token should remain valid (seconds). +# Optional. Defaults to 3600 (1 hour). +# [token_format] Deprecated: Use token_provider instead. +# [cache_dir] Directory created when token_provider is pki. Optional. +# Defaults to /var/cache/keystone. +# [memcache_servers] List of memcache servers/ports. Optional. Used with +# token_driver keystone.token.backends.memcache.Token. Defaults to false. +# [enabled] If the keystone services should be enabled. Optional. Default to true. +# +# [*database_connection*] +# (optional) Url used to connect to database. +# Defaults to sqlite:////var/lib/keystone/keystone.db +# +# [*sql_connection*] +# (optional) Deprecated. Use database_connection instead. +# +# [*database_idle_timeout*] +# (optional) Timeout when db connections should be reaped. +# Defaults to 200. +# +# [*idle_timeout*] +# (optional) Deprecated. Use database_idle_timeout instead. +# +# [enable_pki_setup] Enable call to pki_setup to generate the cert for signing pki tokens and +# revocation lists if it doesn't already exist. This generates a cert and key stored in file +# locations based on the signing_certfile and signing_keyfile paramters below. If you are +# providing your own signing cert, make this false. +# [signing_certfile] Location of the cert file for signing pki tokens and revocation lists. +# Optional. Note that if this file already exists (i.e. you are providing your own signing cert), +# the file will not be overwritten, even if enable_pki_setup is set to true. +# Default: /etc/keystone/ssl/certs/signing_cert.pem +# [signing_keyfile] Location of the key file for signing pki tokens and revocation lists. Optional. +# Note that if this file already exists (i.e. you are providing your own signing cert), the file +# will not be overwritten, even if enable_pki_setup is set to true. +# Default: /etc/keystone/ssl/private/signing_key.pem +# [signing_ca_certs] Use this CA certs file along with signing_certfile/signing_keyfile for +# signing pki tokens and revocation lists. Optional. Default: /etc/keystone/ssl/certs/ca.pem +# [signing_ca_key] Use this CA key file along with signing_certfile/signing_keyfile for signing +# pki tokens and revocation lists. Optional. Default: /etc/keystone/ssl/private/cakey.pem +# +# [rabbit_host] Location of rabbitmq installation. Optional. Defaults to localhost. +# [rabbit_port] Port for rabbitmq instance. Optional. Defaults to 5672. +# [rabbit_hosts] Location of rabbitmq installation. Optional. Defaults to undef. +# [rabbit_password] Password used to connect to rabbitmq. Optional. Defaults to guest. +# [rabbit_userid] User used to connect to rabbitmq. Optional. Defaults to guest. +# [rabbit_virtual_host] The RabbitMQ virtual host. Optional. Defaults to /. +# +# [*rabbit_use_ssl*] +# (optional) Connect over SSL for RabbitMQ +# Defaults to false +# +# [*kombu_ssl_ca_certs*] +# (optional) SSL certification authority file (valid only if SSL enabled). +# Defaults to undef +# +# [*kombu_ssl_certfile*] +# (optional) SSL cert file (valid only if SSL enabled). +# Defaults to undef +# +# [*kombu_ssl_keyfile*] +# (optional) SSL key file (valid only if SSL enabled). +# Defaults to undef +# +# [*kombu_ssl_version*] +# (optional) SSL version to use (valid only if SSL enabled). +# Valid values are TLSv1, SSLv23 and SSLv3. SSLv2 may be +# available on some distributions. +# Defaults to 'SSLv3' +# +# [notification_driver] RPC driver. Not enabled by default +# [notification_topics] AMQP topics to publish to when using the RPC notification driver. +# [control_exchange] AMQP exchange to connect to if using RabbitMQ or Qpid +# +# [*public_bind_host*] +# (optional) The IP address of the public network interface to listen on +# Deprecates bind_host +# Default to '0.0.0.0'. +# +# [*admin_bind_host*] +# (optional) The IP address of the public network interface to listen on +# Deprecates bind_host +# Default to '0.0.0.0'. +# +# [*log_dir*] +# (optional) Directory where logs should be stored +# If set to boolean false, it will not log to any directory +# Defaults to '/var/log/keystone' +# +# [*log_file*] +# (optional) Where to log +# Defaults to false +# +# [*public_endpoint*] +# (optional) The base public endpoint URL for keystone that are +# advertised to clients (NOTE: this does NOT affect how +# keystone listens for connections) (string value) +# If set to false, no public_endpoint will be defined in keystone.conf. +# Sample value: 'http://localhost:5000/' +# Defaults to false +# +# [*admin_endpoint*] +# (optional) The base admin endpoint URL for keystone that are +# advertised to clients (NOTE: this does NOT affect how keystone listens +# for connections) (string value) +# If set to false, no admin_endpoint will be defined in keystone.conf. +# Sample value: 'http://localhost:35357/' +# Defaults to false +# +# [*enable_ssl*] +# (optional) Toggle for SSL support on the keystone eventlet servers. +# (boolean value) +# Defaults to false +# +# [*ssl_certfile*] +# (optional) Path of the certfile for SSL. (string value) +# Defaults to '/etc/keystone/ssl/certs/keystone.pem' +# +# [*ssl_keyfile*] +# (optional) Path of the keyfile for SSL. (string value) +# Defaults to '/etc/keystone/ssl/private/keystonekey.pem' +# +# [*ssl_ca_certs*] +# (optional) Path of the ca cert file for SSL. (string value) +# Defaults to '/etc/keystone/ssl/certs/ca.pem' +# +# [*ssl_ca_key*] +# (optional) Path of the CA key file for SSL (string value) +# Defaults to '/etc/keystone/ssl/private/cakey.pem' +# +# [*ssl_cert_subject*] +# (optional) SSL Certificate Subject (auto generated certificate) +# (string value) +# Defaults to '/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost' +# +# [*mysql_module*] +# (optional) Deprecated. Does nothing. +# +# [*validate_service*] +# (optional) Whether to validate keystone connections after +# the service is started. +# Defaults to false +# +# [*validate_insecure*] +# (optional) Whether to validate keystone connections +# using the --insecure option with keystone client. +# Defaults to false +# +# [*validate_cacert*] +# (optional) Whether to validate keystone connections +# using the specified argument with the --os-cacert option +# with keystone client. +# Defaults to undef +# +# [*validate_auth_url*] +# (optional) The url to validate keystone against +# Defaults to undef +# +# [*service_provider*] +# (optional) Provider, that can be used for keystone service. +# Default value defined in keystone::params for given operation system. +# If you use Pacemaker or another Cluster Resource Manager, you can make +# custom service provider for changing start/stop/status behavior of service, +# and set it here. +# +# [*service_name*] +# (optional) Name of the service that will be providing the +# server functionality of keystone. For example, the default +# is just 'keystone', which means keystone will be run as a +# standalone eventlet service, and will able to be managed +# separately by the operating system's service manager. For +# example, you will be able to use +# service openstack-keystone restart +# to restart the service. +# If the value is 'httpd', this means keystone will be a web +# service, and you must use another class to configure that +# web service. For example, after calling class {'keystone'...} +# use class { 'keystone::wsgi::apache'...} to make keystone be +# a web app using apache mod_wsgi. +# Defaults to 'keystone' +# NOTE: validate_service only applies if the value is 'keystone' +# +# == Dependencies +# None +# +# == Examples +# +# class { 'keystone': +# log_verbose => 'True', +# admin_token => 'my_special_token', +# } +# +# OR +# +# class { 'keystone': +# ... +# service_name => 'httpd', +# ... +# } +# class { 'keystone::wsgi::apache': +# ... +# } +# +# == Authors +# +# Dan Bode dan@puppetlabs.com +# +# == Copyright +# +# Copyright 2012 Puppetlabs Inc, unless otherwise noted. +# +class keystone( + $admin_token, + $package_ensure = 'present', + $bind_host = false, + $public_bind_host = '0.0.0.0', + $admin_bind_host = '0.0.0.0', + $public_port = '5000', + $admin_port = '35357', + $compute_port = '8774', + $verbose = false, + $debug = false, + $log_dir = '/var/log/keystone', + $log_file = false, + $use_syslog = false, + $log_facility = 'LOG_USER', + $catalog_type = 'sql', + $catalog_driver = false, + $catalog_template_file = '/etc/keystone/default_catalog.templates', + $token_format = false, + $token_provider = 'keystone.token.providers.uuid.Provider', + $token_driver = 'keystone.token.backends.sql.Token', + $token_expiration = 3600, + $public_endpoint = false, + $admin_endpoint = false, + $enable_ssl = false, + $ssl_certfile = '/etc/keystone/ssl/certs/keystone.pem', + $ssl_keyfile = '/etc/keystone/ssl/private/keystonekey.pem', + $ssl_ca_certs = '/etc/keystone/ssl/certs/ca.pem', + $ssl_ca_key = '/etc/keystone/ssl/private/cakey.pem', + $ssl_cert_subject = '/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost', + $cache_dir = '/var/cache/keystone', + $memcache_servers = false, + $enabled = true, + $database_connection = 'sqlite:////var/lib/keystone/keystone.db', + $database_idle_timeout = '200', + $enable_pki_setup = true, + $signing_certfile = '/etc/keystone/ssl/certs/signing_cert.pem', + $signing_keyfile = '/etc/keystone/ssl/private/signing_key.pem', + $signing_ca_certs = '/etc/keystone/ssl/certs/ca.pem', + $signing_ca_key = '/etc/keystone/ssl/private/cakey.pem', + $rabbit_host = 'localhost', + $rabbit_hosts = false, + $rabbit_password = 'guest', + $rabbit_port = '5672', + $rabbit_userid = 'guest', + $rabbit_virtual_host = '/', + $rabbit_use_ssl = false, + $kombu_ssl_ca_certs = undef, + $kombu_ssl_certfile = undef, + $kombu_ssl_keyfile = undef, + $kombu_ssl_version = 'SSLv3', + $notification_driver = false, + $notification_topics = false, + $control_exchange = false, + $validate_service = false, + $validate_insecure = false, + $validate_auth_url = false, + $validate_cacert = undef, + $service_provider = $::keystone::params::service_provider, + $service_name = 'keystone', + # DEPRECATED PARAMETERS + $mysql_module = undef, + $sql_connection = undef, + $idle_timeout = undef, +) inherits keystone::params { + + if ! $catalog_driver { + validate_re($catalog_type, 'template|sql') + } + + if $mysql_module { + warning('The mysql_module parameter is deprecated. The latest 2.x mysql module will be used.') + } + + if $sql_connection { + warning('The sql_connection parameter is deprecated, use database_connection instead.') + $database_connection_real = $sql_connection + } else { + $database_connection_real = $database_connection + } + + if $idle_timeout { + warning('The idle_timeout parameter is deprecated, use database_idle_timeout instead.') + $database_idle_timeout_real = $idle_timeout + } else { + $database_idle_timeout_real = $database_idle_timeout + } + + if ($admin_endpoint and 'v2.0' in $admin_endpoint) { + warning('Version string /v2.0/ should not be included in keystone::admin_endpoint') + } + + if ($public_endpoint and 'v2.0' in $public_endpoint) { + warning('Version string /v2.0/ should not be included in keystone::public_endpoint') + } + + if $rabbit_use_ssl { + if !$kombu_ssl_ca_certs { + fail('The kombu_ssl_ca_certs parameter is required when rabbit_use_ssl is set to true') + } + if !$kombu_ssl_certfile { + fail('The kombu_ssl_certfile parameter is required when rabbit_use_ssl is set to true') + } + if !$kombu_ssl_keyfile { + fail('The kombu_ssl_keyfile parameter is required when rabbit_use_ssl is set to true') + } + } + + File['/etc/keystone/keystone.conf'] -> Keystone_config<||> ~> Service[$service_name] + Keystone_config<||> ~> Exec<| title == 'keystone-manage db_sync'|> + Keystone_config<||> ~> Exec<| title == 'keystone-manage pki_setup'|> + include ::keystone::params + + package { 'keystone': + ensure => $package_ensure, + name => $::keystone::params::package_name, + } + + group { 'keystone': + ensure => present, + system => true, + require => Package['keystone'], + } + + user { 'keystone': + ensure => 'present', + gid => 'keystone', + system => true, + require => Package['keystone'], + } + + file { ['/etc/keystone', '/var/log/keystone', '/var/lib/keystone']: + ensure => directory, + mode => '0750', + owner => 'keystone', + group => 'keystone', + require => Package['keystone'], + notify => Service[$service_name], + } + + file { '/etc/keystone/keystone.conf': + ensure => present, + mode => '0600', + owner => 'keystone', + group => 'keystone', + require => Package['keystone'], + notify => Service[$service_name], + } + + if $bind_host { + warning('The bind_host parameter is deprecated, use public_bind_host and admin_bind_host instead.') + $public_bind_host_real = $bind_host + $admin_bind_host_real = $bind_host + } else { + $public_bind_host_real = $public_bind_host + $admin_bind_host_real = $admin_bind_host + } + + # default config + keystone_config { + 'DEFAULT/admin_token': value => $admin_token, secret => true; + 'DEFAULT/public_bind_host': value => $public_bind_host_real; + 'DEFAULT/admin_bind_host': value => $admin_bind_host_real; + 'DEFAULT/public_port': value => $public_port; + 'DEFAULT/admin_port': value => $admin_port; + 'DEFAULT/compute_port': value => $compute_port; + 'DEFAULT/verbose': value => $verbose; + 'DEFAULT/debug': value => $debug; + } + + # Endpoint configuration + if $public_endpoint { + keystone_config { + 'DEFAULT/public_endpoint': value => $public_endpoint; + } + } else { + keystone_config { + 'DEFAULT/public_endpoint': ensure => absent; + } + } + if $admin_endpoint { + keystone_config { + 'DEFAULT/admin_endpoint': value => $admin_endpoint; + } + } else { + keystone_config { + 'DEFAULT/admin_endpoint': ensure => absent; + } + } + # requirements for memcache token driver + if ($token_driver =~ /memcache/ ) { + package { 'python-memcache': + ensure => present, + name => $::keystone::params::python_memcache_package_name, + } + } + + # token driver config + keystone_config { + 'token/driver': value => $token_driver; + 'token/expiration': value => $token_expiration; + } + + # ssl config + if ($enable_ssl) { + keystone_config { + 'ssl/enable': value => true; + 'ssl/certfile': value => $ssl_certfile; + 'ssl/keyfile': value => $ssl_keyfile; + 'ssl/ca_certs': value => $ssl_ca_certs; + 'ssl/ca_key': value => $ssl_ca_key; + 'ssl/cert_subject': value => $ssl_cert_subject; + } + } else { + keystone_config { + 'ssl/enable': value => false; + } + } + + if($database_connection_real =~ /mysql:\/\/\S+:\S+@\S+\/\S+/) { + require 'mysql::bindings' + require 'mysql::bindings::python' + } elsif($database_connection_real =~ /postgresql:\/\/\S+:\S+@\S+\/\S+/) { + + } elsif($database_connection_real =~ /sqlite:\/\//) { + + } else { + fail("Invalid db connection ${database_connection_real}") + } + + # memcache connection config + if $memcache_servers { + validate_array($memcache_servers) + keystone_config { + 'memcache/servers': value => join($memcache_servers, ','); + } + } else { + keystone_config { + 'memcache/servers': ensure => absent; + } + } + + # db connection config + keystone_config { + 'database/connection': value => $database_connection_real, secret => true; + 'database/idle_timeout': value => $database_idle_timeout_real; + } + + # configure based on the catalog backend + if $catalog_driver { + $catalog_driver_real = $catalog_driver + } + elsif ($catalog_type == 'template') { + $catalog_driver_real = 'keystone.catalog.backends.templated.Catalog' + } + elsif ($catalog_type == 'sql') { + $catalog_driver_real = 'keystone.catalog.backends.sql.Catalog' + } + + keystone_config { + 'catalog/driver': value => $catalog_driver_real; + 'catalog/template_file': value => $catalog_template_file; + } + + if $token_format { + warning('token_format parameter is deprecated. Use token_provider instead.') + } + + # remove the old format in case of an upgrade + keystone_config { 'signing/token_format': ensure => absent } + + # Set the signing key/cert configuration values. + keystone_config { + 'signing/certfile': value => $signing_certfile; + 'signing/keyfile': value => $signing_keyfile; + 'signing/ca_certs': value => $signing_ca_certs; + 'signing/ca_key': value => $signing_ca_key; + } + + # Create cache directory used for signing. + file { $cache_dir: + ensure => directory, + } + + # Only do pki_setup if we were asked to do so. This is needed + # regardless of the token provider since token revocation lists + # are always signed. + if $enable_pki_setup { + exec { 'keystone-manage pki_setup': + path => '/usr/bin', + user => 'keystone', + refreshonly => true, + creates => $signing_keyfile, + notify => Service[$service_name], + subscribe => Package['keystone'], + require => User['keystone'], + } + } + + if ($token_format == false and $token_provider == 'keystone.token.providers.pki.Provider') or $token_format == 'PKI' { + keystone_config { 'token/provider': value => 'keystone.token.providers.pki.Provider' } + } elsif $token_format == 'UUID' { + keystone_config { 'token/provider': value => 'keystone.token.providers.uuid.Provider' } + } else { + keystone_config { 'token/provider': value => $token_provider } + } + + if $notification_driver { + keystone_config { 'DEFAULT/notification_driver': value => $notification_driver } + } else { + keystone_config { 'DEFAULT/notification_driver': ensure => absent } + } + if $notification_topics { + keystone_config { 'DEFAULT/notification_topics': value => $notification_topics } + } else { + keystone_config { 'DEFAULT/notification_topics': ensure => absent } + } + if $control_exchange { + keystone_config { 'DEFAULT/control_exchange': value => $control_exchange } + } else { + keystone_config { 'DEFAULT/control_exchange': ensure => absent } + } + + keystone_config { + 'DEFAULT/rabbit_password': value => $rabbit_password, secret => true; + 'DEFAULT/rabbit_userid': value => $rabbit_userid; + 'DEFAULT/rabbit_virtual_host': value => $rabbit_virtual_host; + } + + if $rabbit_hosts { + keystone_config { 'DEFAULT/rabbit_hosts': value => join($rabbit_hosts, ',') } + keystone_config { 'DEFAULT/rabbit_ha_queues': value => true } + } else { + keystone_config { 'DEFAULT/rabbit_host': value => $rabbit_host } + keystone_config { 'DEFAULT/rabbit_port': value => $rabbit_port } + keystone_config { 'DEFAULT/rabbit_hosts': value => "${rabbit_host}:${rabbit_port}" } + keystone_config { 'DEFAULT/rabbit_ha_queues': value => false } + } + + keystone_config { 'DEFAULT/rabbit_use_ssl': value => $rabbit_use_ssl } + if $rabbit_use_ssl { + keystone_config { + 'DEFAULT/kombu_ssl_ca_certs': value => $kombu_ssl_ca_certs; + 'DEFAULT/kombu_ssl_certfile': value => $kombu_ssl_certfile; + 'DEFAULT/kombu_ssl_keyfile': value => $kombu_ssl_keyfile; + 'DEFAULT/kombu_ssl_version': value => $kombu_ssl_version; + } + } else { + keystone_config { + 'DEFAULT/kombu_ssl_ca_certs': ensure => absent; + 'DEFAULT/kombu_ssl_certfile': ensure => absent; + 'DEFAULT/kombu_ssl_keyfile': ensure => absent; + 'DEFAULT/kombu_ssl_version': ensure => absent; + } + } + + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + + if $service_name == 'keystone' { + if $validate_service { + if $validate_auth_url { + $v_auth_url = $validate_auth_url + } else { + $v_auth_url = $admin_endpoint + } + + class { 'keystone::service': + ensure => $service_ensure, + service_name => $::keystone::params::service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + provider => $service_provider, + validate => true, + admin_endpoint => $v_auth_url, + admin_token => $admin_token, + insecure => $validate_insecure, + cacert => $validate_cacert, + } + } else { + class { 'keystone::service': + ensure => $service_ensure, + service_name => $::keystone::params::service_name, + enable => $enabled, + hasstatus => true, + hasrestart => true, + provider => $service_provider, + validate => false, + } + } + } + + if $enabled { + include ::keystone::db::sync + Class['::keystone::db::sync'] ~> Service[$service_name] + } + + # Syslog configuration + if $use_syslog { + keystone_config { + 'DEFAULT/use_syslog': value => true; + 'DEFAULT/syslog_log_facility': value => $log_facility; + } + } else { + keystone_config { + 'DEFAULT/use_syslog': value => false; + } + } + + if $log_file { + keystone_config { + 'DEFAULT/log_file': value => $log_file; + 'DEFAULT/log_dir': value => $log_dir; + } + } else { + if $log_dir { + keystone_config { + 'DEFAULT/log_dir': value => $log_dir; + 'DEFAULT/log_file': ensure => absent; + } + } else { + keystone_config { + 'DEFAULT/log_dir': ensure => absent; + 'DEFAULT/log_file': ensure => absent; + } + } + } + +} diff --git a/keystone/manifests/ldap.pp b/keystone/manifests/ldap.pp new file mode 100644 index 000000000..a065e5468 --- /dev/null +++ b/keystone/manifests/ldap.pp @@ -0,0 +1,181 @@ +# +# Implements ldap configuration for keystone. +# +# == Dependencies +# == Examples +# == Authors +# +# Dan Bode dan@puppetlabs.com +# Matt Fischer matt.fischer@twcable.com +# +# == Copyright +# +# Copyright 2012 Puppetlabs Inc, unless otherwise noted. +# +class keystone::ldap( + $url = undef, + $user = undef, + $password = undef, + $suffix = undef, + $query_scope = undef, + $page_size = undef, + $user_tree_dn = undef, + $user_filter = undef, + $user_objectclass = undef, + $user_id_attribute = undef, + $user_name_attribute = undef, + $user_mail_attribute = undef, + $user_enabled_attribute = undef, + $user_enabled_mask = undef, + $user_enabled_default = undef, + $user_attribute_ignore = undef, + $user_default_project_id_attribute = undef, + $user_allow_create = undef, + $user_allow_update = undef, + $user_allow_delete = undef, + $user_pass_attribute = undef, + $user_enabled_emulation = undef, + $user_enabled_emulation_dn = undef, + $user_additional_attribute_mapping = undef, + $tenant_tree_dn = undef, + $tenant_filter = undef, + $tenant_objectclass = undef, + $tenant_id_attribute = undef, + $tenant_member_attribute = undef, + $tenant_desc_attribute = undef, + $tenant_name_attribute = undef, + $tenant_enabled_attribute = undef, + $tenant_domain_id_attribute = undef, + $tenant_attribute_ignore = undef, + $tenant_allow_create = undef, + $tenant_allow_update = undef, + $tenant_allow_delete = undef, + $tenant_enabled_emulation = undef, + $tenant_enabled_emulation_dn = undef, + $tenant_additional_attribute_mapping = undef, + $role_tree_dn = undef, + $role_filter = undef, + $role_objectclass = undef, + $role_id_attribute = undef, + $role_name_attribute = undef, + $role_member_attribute = undef, + $role_attribute_ignore = undef, + $role_allow_create = undef, + $role_allow_update = undef, + $role_allow_delete = undef, + $role_additional_attribute_mapping = undef, + $group_tree_dn = undef, + $group_filter = undef, + $group_objectclass = undef, + $group_id_attribute = undef, + $group_name_attribute = undef, + $group_member_attribute = undef, + $group_desc_attribute = undef, + $group_attribute_ignore = undef, + $group_allow_create = undef, + $group_allow_update = undef, + $group_allow_delete = undef, + $group_additional_attribute_mapping = undef, + $use_tls = undef, + $tls_cacertdir = undef, + $tls_cacertfile = undef, + $tls_req_cert = undef, + $identity_driver = undef, + $assignment_driver = undef, +) { + + package { 'python-ldap': + ensure => present, + } + + # check for some common driver name mistakes + if ($assignment_driver != undef) { + if ! ($assignment_driver =~ /^keystone.assignment.backends.*Assignment$/) { + fail('assigment driver should be of the form \'keystone.assignment.backends.*Assignment\'') + } + } + + if ($identity_driver != undef) { + if ! ($identity_driver =~ /^keystone.identity.backends.*Identity$/) { + fail('identity driver should be of the form \'keystone.identity.backends.*Identity\'') + } + } + + if ($tls_cacertdir != undef) { + file { $tls_cacertdir: + ensure => directory + } + } + + keystone_config { + 'ldap/url': value => $url; + 'ldap/user': value => $user; + 'ldap/password': value => $password, secret => true; + 'ldap/suffix': value => $suffix; + 'ldap/query_scope': value => $query_scope; + 'ldap/page_size': value => $page_size; + 'ldap/user_tree_dn': value => $user_tree_dn; + 'ldap/user_filter': value => $user_filter; + 'ldap/user_objectclass': value => $user_objectclass; + 'ldap/user_id_attribute': value => $user_id_attribute; + 'ldap/user_name_attribute': value => $user_name_attribute; + 'ldap/user_mail_attribute': value => $user_mail_attribute; + 'ldap/user_enabled_attribute': value => $user_enabled_attribute; + 'ldap/user_enabled_mask': value => $user_enabled_mask; + 'ldap/user_enabled_default': value => $user_enabled_default; + 'ldap/user_attribute_ignore': value => $user_attribute_ignore; + 'ldap/user_default_project_id_attribute': value => $user_default_project_id_attribute; + 'ldap/user_allow_create': value => $user_allow_create; + 'ldap/user_allow_update': value => $user_allow_update; + 'ldap/user_allow_delete': value => $user_allow_delete; + 'ldap/user_pass_attribute': value => $user_pass_attribute; + 'ldap/user_enabled_emulation': value => $user_enabled_emulation; + 'ldap/user_enabled_emulation_dn': value => $user_enabled_emulation_dn; + 'ldap/user_additional_attribute_mapping': value => $user_additional_attribute_mapping; + 'ldap/tenant_tree_dn': value => $tenant_tree_dn; + 'ldap/tenant_filter': value => $tenant_filter; + 'ldap/tenant_objectclass': value => $tenant_objectclass; + 'ldap/tenant_id_attribute': value => $tenant_id_attribute; + 'ldap/tenant_member_attribute': value => $tenant_member_attribute; + 'ldap/tenant_desc_attribute': value => $tenant_desc_attribute; + 'ldap/tenant_name_attribute': value => $tenant_name_attribute; + 'ldap/tenant_enabled_attribute': value => $tenant_enabled_attribute; + 'ldap/tenant_attribute_ignore': value => $tenant_attribute_ignore; + 'ldap/tenant_domain_id_attribute': value => $tenant_domain_id_attribute; + 'ldap/tenant_allow_create': value => $tenant_allow_create; + 'ldap/tenant_allow_update': value => $tenant_allow_update; + 'ldap/tenant_allow_delete': value => $tenant_allow_delete; + 'ldap/tenant_enabled_emulation': value => $tenant_enabled_emulation; + 'ldap/tenant_enabled_emulation_dn': value => $tenant_enabled_emulation_dn; + 'ldap/tenant_additional_attribute_mapping': value => $tenant_additional_attribute_mapping; + 'ldap/role_tree_dn': value => $role_tree_dn; + 'ldap/role_filter': value => $role_filter; + 'ldap/role_objectclass': value => $role_objectclass; + 'ldap/role_id_attribute': value => $role_id_attribute; + 'ldap/role_name_attribute': value => $role_name_attribute; + 'ldap/role_member_attribute': value => $role_member_attribute; + 'ldap/role_attribute_ignore': value => $role_attribute_ignore; + 'ldap/role_allow_create': value => $role_allow_create; + 'ldap/role_allow_update': value => $role_allow_update; + 'ldap/role_allow_delete': value => $role_allow_delete; + 'ldap/role_additional_attribute_mapping': value => $role_additional_attribute_mapping; + 'ldap/group_tree_dn': value => $group_tree_dn; + 'ldap/group_filter': value => $group_filter; + 'ldap/group_objectclass': value => $group_objectclass; + 'ldap/group_id_attribute': value => $group_id_attribute; + 'ldap/group_name_attribute': value => $group_name_attribute; + 'ldap/group_member_attribute': value => $group_member_attribute; + 'ldap/group_desc_attribute': value => $group_desc_attribute; + 'ldap/group_attribute_ignore': value => $group_attribute_ignore; + 'ldap/group_allow_create': value => $group_allow_create; + 'ldap/group_allow_update': value => $group_allow_update; + 'ldap/group_allow_delete': value => $group_allow_delete; + 'ldap/group_additional_attribute_mapping': value => $group_additional_attribute_mapping; + 'ldap/use_tls': value => $use_tls; + 'ldap/tls_cacertdir': value => $tls_cacertdir; + 'ldap/tls_cacertfile': value => $tls_cacertfile; + 'ldap/tls_req_cert': value => $tls_req_cert; + 'identity/driver': value => $identity_driver; + 'assignment/driver': value => $assignment_driver; + } +} diff --git a/keystone/manifests/logging.pp b/keystone/manifests/logging.pp new file mode 100644 index 000000000..aa355c88d --- /dev/null +++ b/keystone/manifests/logging.pp @@ -0,0 +1,208 @@ +# Class keystone::logging +# +# keystone extended logging configuration +# +# == parameters +# +# [*logging_context_format_string*] +# (optional) Format string to use for log messages with context. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s\ +# [%(request_id)s %(user_identity)s] %(instance)s%(message)s' +# +# [*logging_default_format_string*] +# (optional) Format string to use for log messages without context. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s\ +# [-] %(instance)s%(message)s' +# +# [*logging_debug_format_suffix*] +# (optional) Formatted data to append to log format when level is DEBUG. +# Defaults to undef. +# Example: '%(funcName)s %(pathname)s:%(lineno)d' +# +# [*logging_exception_prefix*] +# (optional) Prefix each line of exception output with this format. +# Defaults to undef. +# Example: '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s' +# +# [*log_config_append*] +# The name of an additional logging configuration file. +# Defaults to undef. +# See https://docs.python.org/2/howto/logging.html +# +# [*default_log_levels*] +# (optional) Hash of logger (keys) and level (values) pairs. +# Defaults to undef. +# Example: +# { 'amqp' => 'WARN', 'amqplib' => 'WARN', 'boto' => 'WARN', +# 'qpid' => 'WARN', 'sqlalchemy' => 'WARN', 'suds' => 'INFO', +# 'iso8601' => 'WARN', +# 'requests.packages.urllib3.connectionpool' => 'WARN' } +# +# [*publish_errors*] +# (optional) Publish error events (boolean value). +# Defaults to undef (false if unconfigured). +# +# [*fatal_deprecations*] +# (optional) Make deprecations fatal (boolean value) +# Defaults to undef (false if unconfigured). +# +# [*instance_format*] +# (optional) If an instance is passed with the log message, format it +# like this (string value). +# Defaults to undef. +# Example: '[instance: %(uuid)s] ' +# +# [*instance_uuid_format*] +# (optional) If an instance UUID is passed with the log message, format +# it like this (string value). +# Defaults to undef. +# Example: instance_uuid_format='[instance: %(uuid)s] ' + +# [*log_date_format*] +# (optional) Format string for %%(asctime)s in log records. +# Defaults to undef. +# Example: 'Y-%m-%d %H:%M:%S' + +class keystone::logging( + $logging_context_format_string = undef, + $logging_default_format_string = undef, + $logging_debug_format_suffix = undef, + $logging_exception_prefix = undef, + $log_config_append = undef, + $default_log_levels = undef, + $publish_errors = undef, + $fatal_deprecations = undef, + $instance_format = undef, + $instance_uuid_format = undef, + $log_date_format = undef, +) { + + if $logging_context_format_string { + keystone_config { + 'DEFAULT/logging_context_format_string' : + value => $logging_context_format_string; + } + } + else { + keystone_config { + 'DEFAULT/logging_context_format_string' : ensure => absent; + } + } + + if $logging_default_format_string { + keystone_config { + 'DEFAULT/logging_default_format_string' : + value => $logging_default_format_string; + } + } + else { + keystone_config { + 'DEFAULT/logging_default_format_string' : ensure => absent; + } + } + + if $logging_debug_format_suffix { + keystone_config { + 'DEFAULT/logging_debug_format_suffix' : + value => $logging_debug_format_suffix; + } + } + else { + keystone_config { + 'DEFAULT/logging_debug_format_suffix' : ensure => absent; + } + } + + if $logging_exception_prefix { + keystone_config { + 'DEFAULT/logging_exception_prefix' : value => $logging_exception_prefix; + } + } + else { + keystone_config { + 'DEFAULT/logging_exception_prefix' : ensure => absent; + } + } + + if $log_config_append { + keystone_config { + 'DEFAULT/log_config_append' : value => $log_config_append; + } + } + else { + keystone_config { + 'DEFAULT/log_config_append' : ensure => absent; + } + } + + if $default_log_levels { + keystone_config { + 'DEFAULT/default_log_levels' : + value => join(sort(join_keys_to_values($default_log_levels, '=')), ','); + } + } + else { + keystone_config { + 'DEFAULT/default_log_levels' : ensure => absent; + } + } + + if $publish_errors { + keystone_config { + 'DEFAULT/publish_errors' : value => $publish_errors; + } + } + else { + keystone_config { + 'DEFAULT/publish_errors' : ensure => absent; + } + } + + if $fatal_deprecations { + keystone_config { + 'DEFAULT/fatal_deprecations' : value => $fatal_deprecations; + } + } + else { + keystone_config { + 'DEFAULT/fatal_deprecations' : ensure => absent; + } + } + + if $instance_format { + keystone_config { + 'DEFAULT/instance_format' : value => $instance_format; + } + } + else { + keystone_config { + 'DEFAULT/instance_format' : ensure => absent; + } + } + + if $instance_uuid_format { + keystone_config { + 'DEFAULT/instance_uuid_format' : value => $instance_uuid_format; + } + } + else { + keystone_config { + 'DEFAULT/instance_uuid_format' : ensure => absent; + } + } + + if $log_date_format { + keystone_config { + 'DEFAULT/log_date_format' : value => $log_date_format; + } + } + else { + keystone_config { + 'DEFAULT/log_date_format' : ensure => absent; + } + } + + +} diff --git a/keystone/manifests/params.pp b/keystone/manifests/params.pp new file mode 100644 index 000000000..f3f0f4d26 --- /dev/null +++ b/keystone/manifests/params.pp @@ -0,0 +1,36 @@ +# +# This class contains the platform differences for keystone +# +class keystone::params { + $client_package_name = 'python-keystone' + + case $::osfamily { + 'Debian': { + $package_name = 'keystone' + $service_name = 'keystone' + $keystone_wsgi_script_path = '/usr/lib/cgi-bin/keystone' + $python_memcache_package_name = 'python-memcache' + case $::operatingsystem { + 'Debian': { + $service_provider = undef + $keystone_wsgi_script_source = '/usr/share/keystone/wsgi.py' + } + default: { + # NOTE: Ubuntu does not currently provide the keystone wsgi script in the + # keystone packages. When Ubuntu does provide the script, change this + # to use the correct path (which I'm assuming will be the same as Debian). + $service_provider = 'upstart' + $keystone_wsgi_script_source = 'puppet:///modules/keystone/httpd/keystone.py' + } + } + } + 'RedHat': { + $package_name = 'openstack-keystone' + $service_name = 'openstack-keystone' + $keystone_wsgi_script_path = '/var/www/cgi-bin/keystone' + $python_memcache_package_name = 'python-memcached' + $service_provider = undef + $keystone_wsgi_script_source = '/usr/share/keystone/keystone.wsgi' + } + } +} diff --git a/keystone/manifests/python.pp b/keystone/manifests/python.pp new file mode 100644 index 000000000..858fd6504 --- /dev/null +++ b/keystone/manifests/python.pp @@ -0,0 +1,15 @@ +# +# installs client python libraries for keystone +# +# +class keystone::python ( + $client_package_name = $keystone::params::client_package_name, + $ensure = 'present' +) inherits keystone::params { + + package { 'python-keystone' : + ensure => $ensure, + name => $client_package_name, + } + +} diff --git a/keystone/manifests/roles/admin.pp b/keystone/manifests/roles/admin.pp new file mode 100644 index 000000000..2405a8266 --- /dev/null +++ b/keystone/manifests/roles/admin.pp @@ -0,0 +1,78 @@ +# +# This class implements some reasonable admin defaults for keystone. +# +# It creates the following keystone objects: +# * service tenant (tenant used by all service users) +# * "admin" tenant (defaults to "openstack") +# * admin user (that defaults to the "admin" tenant) +# * admin role +# * adds admin role to admin user on the "admin" tenant +# +# [*Parameters*] +# +# [email] The email address for the admin. Required. +# [password] The admin password. Required. +# [admin_tenant] The name of the tenant to be used for admin privileges. Optional. Defaults to openstack. +# [admin] Admin user. Optional. Defaults to admin. +# [ignore_default_tenant] Ignore setting the default tenant value when the user is created. Optional. Defaults to false. +# [admin_tenant_desc] Optional. Description for admin tenant, defaults to 'admin tenant' +# [service_tenant_desc] Optional. Description for admin tenant, defaults to 'Tenant for the openstack services' +# [configure_user] Optional. Should the admin user be created? Defaults to 'true'. +# [configure_user_role] Optional. Should the admin role be configured for the admin user? Defaulst to 'true'. +# +# == Dependencies +# == Examples +# == Authors +# +# Dan Bode dan@puppetlabs.com +# +# == Copyright +# +# Copyright 2012 Puppetlabs Inc, unless otherwise noted. +# +class keystone::roles::admin( + $email, + $password, + $admin = 'admin', + $admin_tenant = 'openstack', + $service_tenant = 'services', + $ignore_default_tenant = false, + $admin_tenant_desc = 'admin tenant', + $service_tenant_desc = 'Tenant for the openstack services', + $configure_user = true, + $configure_user_role = true, +) { + + keystone_tenant { $service_tenant: + ensure => present, + enabled => true, + description => $service_tenant_desc, + } + keystone_tenant { $admin_tenant: + ensure => present, + enabled => true, + description => $admin_tenant_desc, + } + keystone_role { 'admin': + ensure => present, + } + + if $configure_user { + keystone_user { $admin: + ensure => present, + enabled => true, + tenant => $admin_tenant, + email => $email, + password => $password, + ignore_default_tenant => $ignore_default_tenant, + } + } + + if $configure_user_role { + keystone_user_role { "${admin}@${admin_tenant}": + ensure => present, + roles => 'admin', + } + } + +} diff --git a/keystone/manifests/service.pp b/keystone/manifests/service.pp new file mode 100644 index 000000000..63c148d3b --- /dev/null +++ b/keystone/manifests/service.pp @@ -0,0 +1,124 @@ +# == Class keystone::service +# +# Encapsulates the keystone service to a class. +# This allows resources that require keystone to +# require this class, which can optionally +# validate that the service can actually accept +# connections. +# +# === Parameters +# +# [*ensure*] +# (optional) The desired state of the keystone service +# Defaults to 'running' +# +# [*service_name*] +# (optional) The name of the keystone service +# Defaults to $::keystone::params::service_name +# +# [*enable*] +# (optional) Whether to enable the keystone service +# Defaults to true +# +# [*hasstatus*] +# (optional) Whether the keystone service has status +# Defaults to true +# +# [*hasrestart*] +# (optional) Whether the keystone service has restart +# Defaults to true +# +# [*provider*] +# (optional) Provider for keystone service +# Defaults to $::keystone::params::service_provider +# +# [*validate*] +# (optional) Whether to validate the service is working +# after any service refreshes +# Defaults to false +# +# [*admin_token*] +# (optional) The admin token to use for validation +# Defaults to undef +# +# [*admin_endpoint*] +# (optional) The admin endpont to use for validation +# Defaults to 'http://localhost:35357/v2.0' +# +# [*retries*] +# (optional) Number of times to retry validation +# Defaults to 10 +# +# [*delay*] +# (optional) Number of seconds between validation attempts +# Defaults to 2 +# +# [*insecure*] +# (optional) Whether to validate keystone connections +# using the --insecure option with keystone client. +# Defaults to false +# +# [*cacert*] +# (optional) Whether to validate keystone connections +# using the specified argument with the --os-cacert option +# with keystone client. +# Defaults to undef +# +class keystone::service( + $ensure = 'running', + $service_name = $::keystone::params::service_name, + $enable = true, + $hasstatus = true, + $hasrestart = true, + $provider = $::keystone::params::service_provider, + $validate = false, + $admin_token = undef, + $admin_endpoint = 'http://localhost:35357/v2.0', + $retries = 10, + $delay = 2, + $insecure = false, + $cacert = undef, +) { + include keystone::params + + service { 'keystone': + ensure => $ensure, + name => $service_name, + enable => $enable, + hasstatus => $hasstatus, + hasrestart => $hasrestart, + provider => $provider + } + + if $insecure { + $insecure_s = '--insecure' + } else { + $insecure_s = '' + } + + if $cacert { + $cacert_s = "--os-cacert ${cacert}" + } else { + $cacert_s = '' + } + + if $validate and $admin_token and $admin_endpoint { + $cmd = "keystone --os-endpoint ${admin_endpoint} --os-token ${admin_token} ${insecure_s} ${cacert_s} user-list" + $catch = 'name' + exec { 'validate_keystone_connection': + path => '/usr/bin:/bin:/usr/sbin:/sbin', + provider => shell, + command => $cmd, + subscribe => Service['keystone'], + refreshonly => true, + tries => $retries, + try_sleep => $delay + } + + Exec['validate_keystone_connection'] -> Keystone_user<||> + Exec['validate_keystone_connection'] -> Keystone_role<||> + Exec['validate_keystone_connection'] -> Keystone_tenant<||> + Exec['validate_keystone_connection'] -> Keystone_service<||> + Exec['validate_keystone_connection'] -> Keystone_endpoint<||> + } +} diff --git a/keystone/manifests/wsgi/apache.pp b/keystone/manifests/wsgi/apache.pp new file mode 100644 index 000000000..b2a3b10c3 --- /dev/null +++ b/keystone/manifests/wsgi/apache.pp @@ -0,0 +1,222 @@ +# +# Class to serve keystone with apache mod_wsgi in place of keystone service +# +# Serving keystone from apache is the recommended way to go for production +# systems as the current keystone implementation is not multi-processor aware, +# thus limiting the performance for concurrent accesses. +# +# See the following URIs for reference: +# https://etherpad.openstack.org/havana-keystone-performance +# http://adam.younglogic.com/2012/03/keystone-should-move-to-apache-httpd/ +# +# When using this class you should disable your keystone service. +# +# == Parameters +# +# [*servername*] +# The servername for the virtualhost. +# Optional. Defaults to $::fqdn +# +# [*public_port*] +# The public port. +# Optional. Defaults to 5000 +# +# [*admin_port*] +# The admin port. +# Optional. Defaults to 35357 +# +# [*bind_host*] +# The host/ip address Apache will listen on. +# Optional. Defaults to undef (listen on all ip addresses). +# +# [*public_path*] +# The prefix for the public endpoint. +# Optional. Defaults to '/' +# +# [*admin_path*] +# The prefix for the admin endpoint. +# Optional. Defaults to '/' +# +# [*ssl*] +# Use ssl ? (boolean) +# Optional. Defaults to true +# +# [*workers*] +# Number of WSGI workers to spawn. +# Optional. Defaults to 1 +# +# [*ssl_cert*] +# [*ssl_key*] +# [*ssl_chain*] +# [*ssl_ca*] +# [*ssl_crl_path*] +# [*ssl_crl*] +# [*ssl_certs_dir*] +# apache::vhost ssl parameters. +# Optional. Default to apache::vhost 'ssl_*' defaults. +# +# == Dependencies +# +# requires Class['apache'] & Class['keystone'] +# +# == Examples +# +# include apache +# +# class { 'keystone::wsgi::apache': } +# +# == Note about ports & paths +# +# When using same port for both endpoints (443 anyone ?), you *MUST* use two +# different public_path & admin_path ! +# +# == Authors +# +# François Charlier +# +# == Copyright +# +# Copyright 2013 eNovance +# +class keystone::wsgi::apache ( + $servername = $::fqdn, + $public_port = 5000, + $admin_port = 35357, + $bind_host = undef, + $public_path = '/', + $admin_path = '/', + $ssl = true, + $workers = 1, + $ssl_cert = undef, + $ssl_key = undef, + $ssl_chain = undef, + $ssl_ca = undef, + $ssl_crl_path = undef, + $ssl_crl = undef, + $ssl_certs_dir = undef, + $threads = $::processorcount, + $priority = '10', +) { + + include ::keystone::params + include ::apache + include ::apache::mod::wsgi + if $ssl { + include ::apache::mod::ssl + } + + Package['keystone'] -> Package['httpd'] + Package['keystone'] ~> Service['httpd'] + Keystone_config <| |> ~> Service['httpd'] + Service['httpd'] -> Keystone_endpoint <| |> + Service['httpd'] -> Keystone_role <| |> + Service['httpd'] -> Keystone_service <| |> + Service['httpd'] -> Keystone_tenant <| |> + Service['httpd'] -> Keystone_user <| |> + Service['httpd'] -> Keystone_user_role <| |> + + ## Sanitize parameters + + # Ensure there's no trailing '/' except if this is also the only character + $public_path_real = regsubst($public_path, '(^/.*)/$', '\1') + # Ensure there's no trailing '/' except if this is also the only character + $admin_path_real = regsubst($admin_path, '(^/.*)/$', '\1') + + if $public_port == $admin_port and $public_path_real == $admin_path_real { + fail('When using the same port for public & private endpoints, public_path and admin_path should be different.') + } + + file { $::keystone::params::keystone_wsgi_script_path: + ensure => directory, + owner => 'keystone', + group => 'keystone', + require => Package['httpd'], + } + + file { 'keystone_wsgi_admin': + ensure => file, + path => "${::keystone::params::keystone_wsgi_script_path}/admin", + source => $::keystone::params::keystone_wsgi_script_source, + owner => 'keystone', + group => 'keystone', + mode => '0644', + # source file provided by keystone package + require => [File[$::keystone::params::keystone_wsgi_script_path], Package['keystone']], + } + + file { 'keystone_wsgi_main': + ensure => file, + path => "${::keystone::params::keystone_wsgi_script_path}/main", + source => $::keystone::params::keystone_wsgi_script_source, + owner => 'keystone', + group => 'keystone', + mode => '0644', + # source file provided by keystone package + require => [File[$::keystone::params::keystone_wsgi_script_path], Package['keystone']], + } + + $wsgi_daemon_process_options = { + user => 'keystone', + group => 'keystone', + processes => $workers, + threads => $threads, + } + $wsgi_script_aliases_main = hash([$public_path_real,"${::keystone::params::keystone_wsgi_script_path}/main"]) + $wsgi_script_aliases_admin = hash([$admin_path_real, "${::keystone::params::keystone_wsgi_script_path}/admin"]) + + if $public_port == $admin_port { + $wsgi_script_aliases_main_real = merge($wsgi_script_aliases_main, $wsgi_script_aliases_admin) + } else { + $wsgi_script_aliases_main_real = $wsgi_script_aliases_main + } + + ::apache::vhost { 'keystone_wsgi_main': + ensure => 'present', + servername => $servername, + ip => $bind_host, + port => $public_port, + docroot => $::keystone::params::keystone_wsgi_script_path, + docroot_owner => 'keystone', + docroot_group => 'keystone', + priority => $priority, + ssl => $ssl, + ssl_cert => $ssl_cert, + ssl_key => $ssl_key, + ssl_chain => $ssl_chain, + ssl_ca => $ssl_ca, + ssl_crl_path => $ssl_crl_path, + ssl_crl => $ssl_crl, + ssl_certs_dir => $ssl_certs_dir, + wsgi_daemon_process => 'keystone_main', + wsgi_daemon_process_options => $wsgi_daemon_process_options, + wsgi_process_group => 'keystone_main', + wsgi_script_aliases => $wsgi_script_aliases_main_real, + require => File['keystone_wsgi_main'], + } + + if $public_port != $admin_port { + ::apache::vhost { 'keystone_wsgi_admin': + ensure => 'present', + servername => $servername, + ip => $bind_host, + port => $admin_port, + docroot => $::keystone::params::keystone_wsgi_script_path, + docroot_owner => 'keystone', + docroot_group => 'keystone', + priority => $priority, + ssl => $ssl, + ssl_cert => $ssl_cert, + ssl_key => $ssl_key, + ssl_chain => $ssl_chain, + ssl_ca => $ssl_ca, + ssl_crl_path => $ssl_crl_path, + ssl_crl => $ssl_crl, + ssl_certs_dir => $ssl_certs_dir, + wsgi_daemon_process => 'keystone_admin', + wsgi_daemon_process_options => $wsgi_daemon_process_options, + wsgi_process_group => 'keystone_admin', + wsgi_script_aliases => $wsgi_script_aliases_admin, + require => File['keystone_wsgi_admin'], + } + } +} diff --git a/keystone/spec/classes/keystone_client_spec.rb b/keystone/spec/classes/keystone_client_spec.rb new file mode 100644 index 000000000..8e13655a9 --- /dev/null +++ b/keystone/spec/classes/keystone_client_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'keystone::client' do + + describe "with default parameters" do + it { should contain_package('python-keystoneclient').with_ensure('present') } + end + + describe "with specified version" do + let :params do + {:ensure => '2013.1'} + end + + it { should contain_package('python-keystoneclient').with_ensure('2013.1') } + end +end diff --git a/keystone/spec/classes/keystone_cron_token_flush_spec.rb b/keystone/spec/classes/keystone_cron_token_flush_spec.rb new file mode 100644 index 000000000..0da4522a1 --- /dev/null +++ b/keystone/spec/classes/keystone_cron_token_flush_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'keystone::cron::token_flush' do + + let :facts do + { :osfamily => 'Debian' } + end + + it 'configures a cron' do + should contain_cron('keystone-manage token_flush').with( + :command => 'keystone-manage token_flush >>/var/log/keystone/keystone-tokenflush.log 2>&1', + :environment => 'PATH=/bin:/usr/bin:/usr/sbin', + :user => 'keystone', + :minute => 1, + :hour => 0, + :monthday => '*', + :month => '*', + :weekday => '*' + ) + end +end diff --git a/keystone/spec/classes/keystone_db_mysql_spec.rb b/keystone/spec/classes/keystone_db_mysql_spec.rb new file mode 100644 index 000000000..f20ea9b2c --- /dev/null +++ b/keystone/spec/classes/keystone_db_mysql_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe 'keystone::db::mysql' do + + let :pre_condition do + [ + 'include mysql::server', + 'include keystone::db::sync' + ] + end + + let :facts do + { :osfamily => 'Debian' } + end + + let :params do + { + 'password' => 'keystone_default_password', + } + end + + describe 'with only required params' do + it { should contain_openstacklib__db__mysql('keystone').with( + 'user' => 'keystone', + 'password_hash' => '*B552157B14BCEDDCEAA06767A012F31BDAA9CE3D', + 'dbname' => 'keystone', + 'host' => '127.0.0.1', + 'charset' => 'utf8' + )} + end + + describe "overriding allowed_hosts param to array" do + let :params do + { + :password => 'keystonepass', + :allowed_hosts => ['127.0.0.1','%'] + } + end + + end + describe "overriding allowed_hosts param to string" do + let :params do + { + :password => 'keystonepass2', + :allowed_hosts => '192.168.1.1' + } + end + + end + + describe "overriding allowed_hosts param equals to host param " do + let :params do + { + :password => 'keystonepass2', + :allowed_hosts => '127.0.0.1' + } + end + + end + +end diff --git a/keystone/spec/classes/keystone_db_postgresql_spec.rb b/keystone/spec/classes/keystone_db_postgresql_spec.rb new file mode 100644 index 000000000..7efe94619 --- /dev/null +++ b/keystone/spec/classes/keystone_db_postgresql_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'keystone::db::postgresql' do + + let :req_params do + {:password => 'pw'} + end + + let :facts do + { + :postgres_default_version => '8.4', + :osfamily => 'RedHat', + } + end + + describe 'with only required params' do + let :params do + req_params + end + it { should contain_postgresql__db('keystone').with( + :user => 'keystone', + :password => 'pw' + ) } + end + +end diff --git a/keystone/spec/classes/keystone_endpoint_spec.rb b/keystone/spec/classes/keystone_endpoint_spec.rb new file mode 100644 index 000000000..61b596abd --- /dev/null +++ b/keystone/spec/classes/keystone_endpoint_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe 'keystone::endpoint' do + + it { should contain_keystone_service('keystone').with( + :ensure => 'present', + :type => 'identity', + :description => 'OpenStack Identity Service' + )} + + describe 'with default parameters' do + it { should contain_keystone_endpoint('RegionOne/keystone').with( + :ensure => 'present', + :public_url => 'http://127.0.0.1:5000/v2.0', + :admin_url => 'http://127.0.0.1:35357/v2.0', + :internal_url => 'http://127.0.0.1:5000/v2.0' + )} + end + + describe 'with overridden parameters' do + + let :params do + { :version => 'v42.6', + :public_url => 'https://identity.some.tld/the/main/endpoint', + :admin_url => 'https://identity-int.some.tld/some/admin/endpoint', + :internal_url => 'https://identity-int.some.tld/some/internal/endpoint' } + end + + it { should contain_keystone_endpoint('RegionOne/keystone').with( + :ensure => 'present', + :public_url => 'https://identity.some.tld/the/main/endpoint/v42.6', + :admin_url => 'https://identity-int.some.tld/some/admin/endpoint/v42.6', + :internal_url => 'https://identity-int.some.tld/some/internal/endpoint/v42.6' + )} + end + + describe 'without internal_url parameter' do + + let :params do + { :public_url => 'https://identity.some.tld/the/main/endpoint' } + end + + it 'internal_url should default to public_url' do + should contain_keystone_endpoint('RegionOne/keystone').with( + :ensure => 'present', + :public_url => 'https://identity.some.tld/the/main/endpoint/v2.0', + :internal_url => 'https://identity.some.tld/the/main/endpoint/v2.0' + ) + end + end + + describe 'with deprecated parameters' do + + let :params do + { :public_address => '10.0.0.1', + :admin_address => '10.0.0.2', + :internal_address => '10.0.0.3', + :public_port => '23456', + :admin_port => '12345', + :region => 'RegionTwo', + :version => 'v3.0' } + end + + it { should contain_keystone_endpoint('RegionTwo/keystone').with( + :ensure => 'present', + :public_url => 'http://10.0.0.1:23456/v3.0', + :admin_url => 'http://10.0.0.2:12345/v3.0', + :internal_url => 'http://10.0.0.3:23456/v3.0' + )} + + describe 'public_address overrides public_url' do + let :params do + { :public_address => '10.0.0.1', + :public_port => '12345', + :public_url => 'http://10.10.10.10:23456/v3.0' } + end + + it { should contain_keystone_endpoint('RegionOne/keystone').with( + :ensure => 'present', + :public_url => 'http://10.0.0.1:12345/v2.0' + )} + end + end + + describe 'with overridden deprecated internal_port' do + + let :params do + { :internal_port => '12345' } + end + + it { should contain_keystone_endpoint('RegionOne/keystone').with( + :ensure => 'present', + :public_url => 'http://127.0.0.1:5000/v2.0', + :admin_url => 'http://127.0.0.1:35357/v2.0', + :internal_url => 'http://127.0.0.1:12345/v2.0' + )} + end + +end diff --git a/keystone/spec/classes/keystone_ldap_spec.rb b/keystone/spec/classes/keystone_ldap_spec.rb new file mode 100644 index 000000000..6d62f56f4 --- /dev/null +++ b/keystone/spec/classes/keystone_ldap_spec.rb @@ -0,0 +1,156 @@ +require 'spec_helper' + +describe 'keystone::ldap' do + describe 'with basic params' do + let :params do + { + :url => 'ldap://foo', + :user => 'cn=foo,dc=example,dc=com', + :password => 'abcdefg', + :suffix => 'dc=example,dc=com', + :query_scope => 'sub', + :page_size => '50', + :user_tree_dn => 'cn=users,dc=example,dc=com', + :user_filter => '(memberOf=cn=openstack,cn=groups,cn=accounts,dc=example,dc=com)', + :user_objectclass => 'inetUser', + :user_id_attribute => 'uid', + :user_name_attribute => 'cn', + :user_mail_attribute => 'mail', + :user_enabled_attribute => 'UserAccountControl', + :user_enabled_mask => '2', + :user_enabled_default => '512', + :user_attribute_ignore => '', + :user_default_project_id_attribute => 'defaultProject', + :user_allow_create => 'False', + :user_allow_update => 'False', + :user_allow_delete => 'False', + :user_pass_attribute => 'krbPassword', + :user_enabled_emulation => 'True', + :user_enabled_emulation_dn => 'cn=openstack-enabled,cn=groups,cn=accounts,dc=example,dc=com', + :user_additional_attribute_mapping => 'description:name, gecos:name', + :tenant_tree_dn => 'ou=projects,ou=openstack,dc=example,dc=com', + :tenant_filter => '', + :tenant_objectclass => 'organizationalUnit', + :tenant_id_attribute => 'ou', + :tenant_member_attribute => 'member', + :tenant_desc_attribute => 'description', + :tenant_name_attribute => 'ou', + :tenant_enabled_attribute => 'enabled', + :tenant_domain_id_attribute => 'businessCategory', + :tenant_attribute_ignore => '', + :tenant_allow_create => 'True', + :tenant_allow_update => 'True', + :tenant_allow_delete => 'True', + :tenant_enabled_emulation => 'False', + :tenant_enabled_emulation_dn => 'True', + :tenant_additional_attribute_mapping => 'cn=enabled,ou=openstack,dc=example,dc=com', + :role_tree_dn => 'ou=roles,ou=openstack,dc=example,dc=com', + :role_filter => '', + :role_objectclass => 'organizationalRole', + :role_id_attribute => 'cn', + :role_name_attribute => 'ou', + :role_member_attribute => 'roleOccupant', + :role_attribute_ignore => 'description', + :role_allow_create => 'True', + :role_allow_update => 'True', + :role_allow_delete => 'True', + :role_additional_attribute_mapping => '', + :group_tree_dn => 'ou=groups,ou=openstack,dc=example,dc=com', + :group_filter => 'cn=enabled-groups,cn=groups,cn=accounts,dc=example,dc=com', + :group_objectclass => 'organizationalRole', + :group_id_attribute => 'cn', + :group_name_attribute => 'cn', + :group_member_attribute => 'roleOccupant', + :group_desc_attribute => 'description', + :group_attribute_ignore => '', + :group_allow_create => 'False', + :group_allow_update => 'False', + :group_allow_delete => 'False', + :group_additional_attribute_mapping => '', + :use_tls => 'False', + :tls_cacertdir => '/etc/ssl/certs/', + :tls_cacertfile => '/etc/ssl/certs/ca-certificates.crt', + :tls_req_cert => 'demand', + :identity_driver => 'keystone.identity.backends.ldap.Identity', + :assignment_driver => 'keystone.assignment.backends.ldap.Assignment', + } + end + it { should contain_package('python-ldap') } + it 'should have basic params' do + should contain_keystone_config('ldap/url').with_value('ldap://foo') + should contain_keystone_config('ldap/user').with_value('cn=foo,dc=example,dc=com') + should contain_keystone_config('ldap/password').with_value('abcdefg').with_secret(true) + should contain_keystone_config('ldap/suffix').with_value('dc=example,dc=com') + should contain_keystone_config('ldap/query_scope').with_value('sub') + should contain_keystone_config('ldap/page_size').with_value('50') + + should contain_keystone_config('ldap/user_tree_dn').with_value('cn=users,dc=example,dc=com') + should contain_keystone_config('ldap/user_filter').with_value('(memberOf=cn=openstack,cn=groups,cn=accounts,dc=example,dc=com)') + should contain_keystone_config('ldap/user_objectclass').with_value('inetUser') + should contain_keystone_config('ldap/user_id_attribute').with_value('uid') + should contain_keystone_config('ldap/user_name_attribute').with_value('cn') + should contain_keystone_config('ldap/user_mail_attribute').with_value('mail') + should contain_keystone_config('ldap/user_enabled_attribute').with_value('UserAccountControl') + should contain_keystone_config('ldap/user_enabled_mask').with_value('2') + should contain_keystone_config('ldap/user_enabled_default').with_value('512') + should contain_keystone_config('ldap/user_attribute_ignore').with_value('') + should contain_keystone_config('ldap/user_default_project_id_attribute').with_value('defaultProject') + should contain_keystone_config('ldap/user_tree_dn').with_value('cn=users,dc=example,dc=com') + should contain_keystone_config('ldap/user_allow_create').with_value('False') + should contain_keystone_config('ldap/user_allow_update').with_value('False') + should contain_keystone_config('ldap/user_allow_delete').with_value('False') + should contain_keystone_config('ldap/user_pass_attribute').with_value('krbPassword') + should contain_keystone_config('ldap/user_enabled_emulation').with_value('True') + should contain_keystone_config('ldap/user_enabled_emulation_dn').with_value('cn=openstack-enabled,cn=groups,cn=accounts,dc=example,dc=com') + should contain_keystone_config('ldap/user_additional_attribute_mapping').with_value('description:name, gecos:name') + + should contain_keystone_config('ldap/tenant_tree_dn').with_value('ou=projects,ou=openstack,dc=example,dc=com') + should contain_keystone_config('ldap/tenant_filter').with_value('') + should contain_keystone_config('ldap/tenant_objectclass').with_value('organizationalUnit') + should contain_keystone_config('ldap/tenant_id_attribute').with_value('ou') + should contain_keystone_config('ldap/tenant_member_attribute').with_value('member') + should contain_keystone_config('ldap/tenant_desc_attribute').with_value('description') + should contain_keystone_config('ldap/tenant_name_attribute').with_value('ou') + should contain_keystone_config('ldap/tenant_enabled_attribute').with_value('enabled') + should contain_keystone_config('ldap/tenant_domain_id_attribute').with_value('businessCategory') + should contain_keystone_config('ldap/tenant_attribute_ignore').with_value('') + should contain_keystone_config('ldap/tenant_allow_create').with_value('True') + should contain_keystone_config('ldap/tenant_allow_update').with_value('True') + should contain_keystone_config('ldap/tenant_allow_delete').with_value('True') + should contain_keystone_config('ldap/tenant_enabled_emulation').with_value('False') + should contain_keystone_config('ldap/tenant_enabled_emulation_dn').with_value('True') + should contain_keystone_config('ldap/tenant_additional_attribute_mapping').with_value('cn=enabled,ou=openstack,dc=example,dc=com') + should contain_keystone_config('ldap/role_tree_dn').with_value('ou=roles,ou=openstack,dc=example,dc=com') + should contain_keystone_config('ldap/role_filter').with_value('') + should contain_keystone_config('ldap/role_objectclass').with_value('organizationalRole') + should contain_keystone_config('ldap/role_id_attribute').with_value('cn') + should contain_keystone_config('ldap/role_name_attribute').with_value('ou') + should contain_keystone_config('ldap/role_member_attribute').with_value('roleOccupant') + should contain_keystone_config('ldap/role_attribute_ignore').with_value('description') + should contain_keystone_config('ldap/role_allow_create').with_value('True') + should contain_keystone_config('ldap/role_allow_update').with_value('True') + should contain_keystone_config('ldap/role_allow_delete').with_value('True') + should contain_keystone_config('ldap/role_additional_attribute_mapping').with_value('') + + should contain_keystone_config('ldap/group_tree_dn').with_value('ou=groups,ou=openstack,dc=example,dc=com') + should contain_keystone_config('ldap/group_filter').with_value('cn=enabled-groups,cn=groups,cn=accounts,dc=example,dc=com') + should contain_keystone_config('ldap/group_objectclass').with_value('organizationalRole') + should contain_keystone_config('ldap/group_id_attribute').with_value('cn') + should contain_keystone_config('ldap/group_member_attribute').with_value('roleOccupant') + should contain_keystone_config('ldap/group_desc_attribute').with_value('description') + should contain_keystone_config('ldap/group_name_attribute').with_value('cn') + should contain_keystone_config('ldap/group_attribute_ignore').with_value('') + should contain_keystone_config('ldap/group_allow_create').with_value('False') + should contain_keystone_config('ldap/group_allow_update').with_value('False') + should contain_keystone_config('ldap/group_allow_delete').with_value('False') + should contain_keystone_config('ldap/group_additional_attribute_mapping').with_value('') + should contain_keystone_config('ldap/use_tls').with_value('False') + should contain_keystone_config('ldap/tls_cacertdir').with_value('/etc/ssl/certs/') + should contain_keystone_config('ldap/tls_cacertfile').with_value('/etc/ssl/certs/ca-certificates.crt') + should contain_keystone_config('ldap/tls_req_cert').with_value('demand') + should contain_keystone_config('identity/driver').with_value('keystone.identity.backends.ldap.Identity') + should contain_keystone_config('assignment/driver').with_value('keystone.assignment.backends.ldap.Assignment') + end + end +end + diff --git a/keystone/spec/classes/keystone_logging_spec.rb b/keystone/spec/classes/keystone_logging_spec.rb new file mode 100644 index 000000000..7ae935201 --- /dev/null +++ b/keystone/spec/classes/keystone_logging_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe 'keystone::logging' do + + let :params do + { + } + end + + let :log_params do + { + :logging_context_format_string => '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s', + :logging_default_format_string => '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s', + :logging_debug_format_suffix => '%(funcName)s %(pathname)s:%(lineno)d', + :logging_exception_prefix => '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s', + :log_config_append => '/etc/keystone/logging.conf', + :publish_errors => true, + :default_log_levels => { + 'amqp' => 'WARN', 'amqplib' => 'WARN', 'boto' => 'WARN', + 'qpid' => 'WARN', 'sqlalchemy' => 'WARN', 'suds' => 'INFO', + 'iso8601' => 'WARN', + 'requests.packages.urllib3.connectionpool' => 'WARN' }, + :fatal_deprecations => true, + :instance_format => '[instance: %(uuid)s] ', + :instance_uuid_format => '[instance: %(uuid)s] ', + :log_date_format => '%Y-%m-%d %H:%M:%S', + } + end + + shared_examples_for 'keystone-logging' do + + context 'with extended logging options' do + before { params.merge!( log_params ) } + it_configures 'logging params set' + end + + context 'without extended logging options' do + it_configures 'logging params unset' + end + + end + + shared_examples_for 'logging params set' do + it 'enables logging params' do + should contain_keystone_config('DEFAULT/logging_context_format_string').with_value( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s') + + should contain_keystone_config('DEFAULT/logging_default_format_string').with_value( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s') + + should contain_keystone_config('DEFAULT/logging_debug_format_suffix').with_value( + '%(funcName)s %(pathname)s:%(lineno)d') + + should contain_keystone_config('DEFAULT/logging_exception_prefix').with_value( + '%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s') + + should contain_keystone_config('DEFAULT/log_config_append').with_value( + '/etc/keystone/logging.conf') + should contain_keystone_config('DEFAULT/publish_errors').with_value( + true) + + should contain_keystone_config('DEFAULT/default_log_levels').with_value( + 'amqp=WARN,amqplib=WARN,boto=WARN,iso8601=WARN,qpid=WARN,requests.packages.urllib3.connectionpool=WARN,sqlalchemy=WARN,suds=INFO') + + should contain_keystone_config('DEFAULT/fatal_deprecations').with_value( + true) + + should contain_keystone_config('DEFAULT/instance_format').with_value( + '[instance: %(uuid)s] ') + + should contain_keystone_config('DEFAULT/instance_uuid_format').with_value( + '[instance: %(uuid)s] ') + + should contain_keystone_config('DEFAULT/log_date_format').with_value( + '%Y-%m-%d %H:%M:%S') + end + end + + + shared_examples_for 'logging params unset' do + [ :logging_context_format_string, :logging_default_format_string, + :logging_debug_format_suffix, :logging_exception_prefix, + :log_config_append, :publish_errors, + :default_log_levels, :fatal_deprecations, + :instance_format, :instance_uuid_format, + :log_date_format, ].each { |param| + it { should contain_keystone_config("DEFAULT/#{param}").with_ensure('absent') } + } + end + + context 'on Debian platforms' do + let :facts do + { :osfamily => 'Debian' } + end + + it_configures 'keystone-logging' + end + + context 'on RedHat platforms' do + let :facts do + { :osfamily => 'RedHat' } + end + + it_configures 'keystone-logging' + end + +end diff --git a/keystone/spec/classes/keystone_python_spec.rb b/keystone/spec/classes/keystone_python_spec.rb new file mode 100644 index 000000000..1324fb2f6 --- /dev/null +++ b/keystone/spec/classes/keystone_python_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'keystone::python' do + + let :facts do + { :osfamily => 'Debian' } + end + + it { should contain_package('python-keystone').with_ensure("present") } + + describe 'override ensure' do + let(:params) { { :ensure => "latest" } } + + it { should contain_package('python-keystone').with_ensure("latest") } + end + +end diff --git a/keystone/spec/classes/keystone_roles_admin_spec.rb b/keystone/spec/classes/keystone_roles_admin_spec.rb new file mode 100644 index 000000000..c1c81e7d2 --- /dev/null +++ b/keystone/spec/classes/keystone_roles_admin_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' +describe 'keystone::roles::admin' do + + describe 'with only the required params set' do + + let :params do + { + :email => 'foo@bar', + :password => 'ChangeMe', + :service_tenant => 'services' + } + end + + it { should contain_keystone_tenant('services').with( + :ensure => 'present', + :enabled => true, + :description => 'Tenant for the openstack services' + )} + it { should contain_keystone_tenant('openstack').with( + :ensure => 'present', + :enabled => true, + :description => 'admin tenant' + )} + it { should contain_keystone_user('admin').with( + :ensure => 'present', + :enabled => true, + :tenant => 'openstack', + :email => 'foo@bar', + :password => 'ChangeMe', + :ignore_default_tenant => 'false' + )} + it { should contain_keystone_role('admin').with_ensure('present') } + it { should contain_keystone_user_role('admin@openstack').with( + :roles => 'admin', + :ensure => 'present' + )} + + end + + describe 'when overriding optional params' do + + let :params do + { + :admin => 'admin', + :email => 'foo@baz', + :password => 'foo', + :admin_tenant => 'admin', + :service_tenant => 'foobar', + :ignore_default_tenant => 'true', + :admin_tenant_desc => 'admin something else', + :service_tenant_desc => 'foobar description', + } + end + + it { should contain_keystone_tenant('foobar').with( + :ensure => 'present', + :enabled => true, + :description => 'foobar description' + )} + it { should contain_keystone_tenant('admin').with( + :ensure => 'present', + :enabled => true, + :description => 'admin something else' + )} + it { should contain_keystone_user('admin').with( + :ensure => 'present', + :enabled => true, + :tenant => 'admin', + :email => 'foo@baz', + :password => 'foo', + :ignore_default_tenant => 'true' + )} + it { should contain_keystone_user_role('admin@admin').with( + :roles => 'admin', + :ensure => 'present' + )} + + end + + describe 'when disabling user configuration' do + before do + let :params do + { + :configure_user => false + } + end + + it { should_not contain_keystone_user('keystone') } + it { should contain_keystone_user_role('keystone@openstack') } + end + end + + describe 'when disabling user and role configuration' do + before do + let :params do + { + :configure_user => false, + :configure_user_role => false + } + end + + it { should_not contain_keystone_user('keystone') } + it { should_not contain_keystone_user_role('keystone@openstack') } + end + end + +end diff --git a/keystone/spec/classes/keystone_service_spec.rb b/keystone/spec/classes/keystone_service_spec.rb new file mode 100644 index 000000000..29d90b0d5 --- /dev/null +++ b/keystone/spec/classes/keystone_service_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe 'keystone::service' do + + describe "with default parameters" do + it { should contain_service('keystone').with( + :ensure => 'running', + :enable => true, + :hasstatus => true, + :hasrestart => true + ) } + it { should_not contain_exec('validate_keystone_connection') } + end + + describe "with validation on" do + let :params do + { + :validate => 'true', + :admin_token => 'admintoken' + } + end + + it { should contain_service('keystone').with( + :ensure => 'running', + :enable => true, + :hasstatus => true, + :hasrestart => true + ) } + it { should contain_exec('validate_keystone_connection') } + end +end diff --git a/keystone/spec/classes/keystone_spec.rb b/keystone/spec/classes/keystone_spec.rb new file mode 100644 index 000000000..c2e97b74e --- /dev/null +++ b/keystone/spec/classes/keystone_spec.rb @@ -0,0 +1,785 @@ +require 'spec_helper' + +describe 'keystone' do + + let :global_facts do + { + :processorcount => 42, + :concat_basedir => '/var/lib/puppet/concat', + :fqdn => 'some.host.tld' + } + end + + let :facts do + global_facts.merge({ + :osfamily => 'Debian', + :operatingsystem => 'Debian', + :operatingsystemrelease => '7.0' + }) + end + + default_params = { + 'admin_token' => 'service_token', + 'package_ensure' => 'present', + 'public_bind_host' => '0.0.0.0', + 'admin_bind_host' => '0.0.0.0', + 'public_port' => '5000', + 'admin_port' => '35357', + 'admin_token' => 'service_token', + 'compute_port' => '8774', + 'verbose' => false, + 'debug' => false, + 'catalog_type' => 'sql', + 'catalog_driver' => false, + 'token_provider' => 'keystone.token.providers.uuid.Provider', + 'token_driver' => 'keystone.token.backends.sql.Token', + 'cache_dir' => '/var/cache/keystone', + 'enable_ssl' => false, + 'ssl_certfile' => '/etc/keystone/ssl/certs/keystone.pem', + 'ssl_keyfile' => '/etc/keystone/ssl/private/keystonekey.pem', + 'ssl_ca_certs' => '/etc/keystone/ssl/certs/ca.pem', + 'ssl_ca_key' => '/etc/keystone/ssl/private/cakey.pem', + 'ssl_cert_subject' => '/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost', + 'enabled' => true, + 'database_connection' => 'sqlite:////var/lib/keystone/keystone.db', + 'database_idle_timeout' => '200', + 'enable_pki_setup' => true, + 'signing_certfile' => '/etc/keystone/ssl/certs/signing_cert.pem', + 'signing_keyfile' => '/etc/keystone/ssl/private/signing_key.pem', + 'signing_ca_certs' => '/etc/keystone/ssl/certs/ca.pem', + 'signing_ca_key' => '/etc/keystone/ssl/private/cakey.pem', + 'rabbit_host' => 'localhost', + 'rabbit_password' => 'guest', + 'rabbit_userid' => 'guest', + } + + override_params = { + 'package_ensure' => 'latest', + 'public_bind_host' => '0.0.0.0', + 'admin_bind_host' => '0.0.0.0', + 'public_port' => '5001', + 'admin_port' => '35358', + 'admin_token' => 'service_token_override', + 'compute_port' => '8778', + 'verbose' => true, + 'debug' => true, + 'catalog_type' => 'template', + 'token_provider' => 'keystone.token.providers.uuid.Provider', + 'token_driver' => 'keystone.token.backends.kvs.Token', + 'public_endpoint' => 'https://localhost:5000/v2.0/', + 'admin_endpoint' => 'https://localhost:35357/v2.0/', + 'enable_ssl' => true, + 'ssl_certfile' => '/etc/keystone/ssl/certs/keystone.pem', + 'ssl_keyfile' => '/etc/keystone/ssl/private/keystonekey.pem', + 'ssl_ca_certs' => '/etc/keystone/ssl/certs/ca.pem', + 'ssl_ca_key' => '/etc/keystone/ssl/private/cakey.pem', + 'ssl_cert_subject' => '/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost', + 'enabled' => false, + 'database_connection' => 'mysql://a:b@c/d', + 'database_idle_timeout' => '300', + 'enable_pki_setup' => true, + 'signing_certfile' => '/etc/keystone/ssl/certs/signing_cert.pem', + 'signing_keyfile' => '/etc/keystone/ssl/private/signing_key.pem', + 'signing_ca_certs' => '/etc/keystone/ssl/certs/ca.pem', + 'signing_ca_key' => '/etc/keystone/ssl/private/cakey.pem', + 'rabbit_host' => '127.0.0.1', + 'rabbit_password' => 'openstack', + 'rabbit_userid' => 'admin', + } + + httpd_params = {'service_name' => 'httpd'}.merge(default_params) + + shared_examples_for 'core keystone examples' do |param_hash| + it { should contain_class('keystone::params') } + + it { should contain_package('keystone').with( + 'ensure' => param_hash['package_ensure'] + ) } + + it { should contain_group('keystone').with( + 'ensure' => 'present', + 'system' => true + ) } + + it { should contain_user('keystone').with( + 'ensure' => 'present', + 'gid' => 'keystone', + 'system' => true + ) } + + it 'should contain the expected directories' do + ['/etc/keystone', '/var/log/keystone', '/var/lib/keystone'].each do |d| + should contain_file(d).with( + 'ensure' => 'directory', + 'owner' => 'keystone', + 'group' => 'keystone', + 'mode' => '0750', + 'require' => 'Package[keystone]' + ) + end + end + + it 'should only synchronize the db if $enabled is true' do + if param_hash['enabled'] + should contain_exec('keystone-manage db_sync').with( + :user => 'keystone', + :refreshonly => true, + :subscribe => ['Package[keystone]', 'Keystone_config[database/connection]'], + :require => 'User[keystone]' + ) + end + end + + it 'should contain correct config' do + [ + 'public_bind_host', + 'admin_bind_host', + 'public_port', + 'admin_port', + 'compute_port', + 'verbose', + 'debug' + ].each do |config| + should contain_keystone_config("DEFAULT/#{config}").with_value(param_hash[config]) + end + end + + it 'should contain correct admin_token config' do + should contain_keystone_config('DEFAULT/admin_token').with_value(param_hash['admin_token']).with_secret(true) + end + + it 'should contain correct mysql config' do + should contain_keystone_config('database/idle_timeout').with_value(param_hash['database_idle_timeout']) + should contain_keystone_config('database/connection').with_value(param_hash['database_connection']).with_secret(true) + end + + it { should contain_keystone_config('token/provider').with_value( + param_hash['token_provider'] + ) } + + it 'should contain correct token driver' do + should contain_keystone_config('token/driver').with_value(param_hash['token_driver']) + end + + it 'should ensure proper setting of admin_endpoint and public_endpoint' do + if param_hash['admin_endpoint'] + should contain_keystone_config('DEFAULT/admin_endpoint').with_value(param_hash['admin_endpoint']) + else + should contain_keystone_config('DEFAULT/admin_endpoint').with_ensure('absent') + end + if param_hash['public_endpoint'] + should contain_keystone_config('DEFAULT/public_endpoint').with_value(param_hash['public_endpoint']) + else + should contain_keystone_config('DEFAULT/public_endpoint').with_ensure('absent') + end + end + + it 'should contain correct rabbit_password' do + should contain_keystone_config('DEFAULT/rabbit_password').with_value(param_hash['rabbit_password']).with_secret(true) + end + end + + [default_params, override_params].each do |param_hash| + describe "when #{param_hash == default_params ? "using default" : "specifying"} class parameters for service" do + + let :params do + param_hash + end + + it_configures 'core keystone examples', param_hash + + it { should contain_service('keystone').with( + 'ensure' => param_hash['enabled'] ? 'running' : 'stopped', + 'enable' => param_hash['enabled'], + 'hasstatus' => true, + 'hasrestart' => true + ) } + + end + end + + describe "when using default class parameters for httpd" do + let :params do + httpd_params + end + + let :pre_condition do + 'include ::apache' + end + + it_configures 'core keystone examples', httpd_params + + it do + expect { + should contain_service('keystone') + }.to raise_error(RSpec::Expectations::ExpectationNotMetError, /expected that the catalogue would contain Service\[keystone\]/) + end + + end + + describe 'with deprecated sql_connection parameter' do + let :params do + { :admin_token => 'service_token', + :sql_connection => 'mysql://a:b@c/d' } + end + + it { should contain_keystone_config('database/connection').with_value(params[:sql_connection]) } + end + + describe 'with deprecated idle_timeout parameter' do + let :params do + { :admin_token => 'service_token', + :idle_timeout => 365 } + end + + it { should contain_keystone_config('database/idle_timeout').with_value(params[:idle_timeout]) } + end + + describe 'when configuring signing token provider' do + + describe 'when configuring as UUID' do + let :params do + { + 'admin_token' => 'service_token', + 'token_provider' => 'keystone.token.providers.uuid.Provider' + } + end + it { should contain_exec('keystone-manage pki_setup').with( + :creates => '/etc/keystone/ssl/private/signing_key.pem' + ) } + it { should contain_file('/var/cache/keystone').with_ensure('directory') } + + describe 'when overriding the cache dir' do + before do + params.merge!(:cache_dir => '/var/lib/cache/keystone') + end + it { should contain_file('/var/lib/cache/keystone') } + end + + describe 'when disable pki_setup' do + before do + params.merge!(:enable_pki_setup => false) + end + it { should_not contain_exec('keystone-manage pki_setup') } + end + end + + describe 'when configuring as PKI' do + let :params do + { + 'admin_token' => 'service_token', + 'token_provider' => 'keystone.token.providers.pki.Provider' + } + end + it { should contain_exec('keystone-manage pki_setup').with( + :creates => '/etc/keystone/ssl/private/signing_key.pem' + ) } + it { should contain_file('/var/cache/keystone').with_ensure('directory') } + + describe 'when overriding the cache dir' do + before do + params.merge!(:cache_dir => '/var/lib/cache/keystone') + end + it { should contain_file('/var/lib/cache/keystone') } + end + + describe 'when disable pki_setup' do + before do + params.merge!(:enable_pki_setup => false) + end + it { should_not contain_exec('keystone-manage pki_setup') } + end + end + + describe 'when configuring PKI signing cert paths with UUID and with pki_setup disabled' do + let :params do + { + 'admin_token' => 'service_token', + 'token_provider' => 'keystone.token.providers.uuid.Provider', + 'enable_pki_setup' => false, + 'signing_certfile' => 'signing_certfile', + 'signing_keyfile' => 'signing_keyfile', + 'signing_ca_certs' => 'signing_ca_certs', + 'signing_ca_key' => 'signing_ca_key' + } + end + + it { should_not contain_exec('keystone-manage pki_setup') } + + it 'should contain correct PKI certfile config' do + should contain_keystone_config('signing/certfile').with_value('signing_certfile') + end + + it 'should contain correct PKI keyfile config' do + should contain_keystone_config('signing/keyfile').with_value('signing_keyfile') + end + + it 'should contain correct PKI ca_certs config' do + should contain_keystone_config('signing/ca_certs').with_value('signing_ca_certs') + end + + it 'should contain correct PKI ca_key config' do + should contain_keystone_config('signing/ca_key').with_value('signing_ca_key') + end + end + + describe 'when configuring PKI signing cert paths with pki_setup disabled' do + let :params do + { + 'admin_token' => 'service_token', + 'token_provider' => 'keystone.token.providers.pki.Provider', + 'enable_pki_setup' => false, + 'signing_certfile' => 'signing_certfile', + 'signing_keyfile' => 'signing_keyfile', + 'signing_ca_certs' => 'signing_ca_certs', + 'signing_ca_key' => 'signing_ca_key' + } + end + + it { should_not contain_exec('keystone-manage pki_setup') } + + it 'should contain correct PKI certfile config' do + should contain_keystone_config('signing/certfile').with_value('signing_certfile') + end + + it 'should contain correct PKI keyfile config' do + should contain_keystone_config('signing/keyfile').with_value('signing_keyfile') + end + + it 'should contain correct PKI ca_certs config' do + should contain_keystone_config('signing/ca_certs').with_value('signing_ca_certs') + end + + it 'should contain correct PKI ca_key config' do + should contain_keystone_config('signing/ca_key').with_value('signing_ca_key') + end + end + + describe 'with invalid catalog_type' do + let :params do + { :admin_token => 'service_token', + :catalog_type => 'invalid' } + end + + it_raises "a Puppet::Error", /validate_re\(\): "invalid" does not match "template|sql"/ + end + + describe 'when configuring catalog driver' do + let :params do + { :admin_token => 'service_token', + :catalog_driver => 'keystone.catalog.backends.alien.AlienCatalog' } + end + + it { should contain_keystone_config('catalog/driver').with_value(params[:catalog_driver]) } + end + + describe 'when configuring deprecated token_format as UUID with enable_pki_setup' do + let :params do + { + 'admin_token' => 'service_token', + 'token_format' => 'UUID' + } + end + it { should contain_exec('keystone-manage pki_setup').with( + :creates => '/etc/keystone/ssl/private/signing_key.pem' + ) } + it { should contain_file('/var/cache/keystone').with_ensure('directory') } + describe 'when overriding the cache dir' do + let :params do + { + 'admin_token' => 'service_token', + 'token_provider' => 'keystone.token.providers.pki.Provider', + 'cache_dir' => '/var/lib/cache/keystone' + } + end + it { should contain_file('/var/lib/cache/keystone') } + end + end + + describe 'when configuring deprecated token_format as UUID without enable_pki_setup' do + let :params do + { + 'admin_token' => 'service_token', + 'token_format' => 'UUID', + 'enable_pki_setup' => false + } + end + it { should_not contain_exec('keystone-manage pki_setup') } + it { should contain_file('/var/cache/keystone').with_ensure('directory') } + describe 'when overriding the cache dir' do + let :params do + { + 'admin_token' => 'service_token', + 'token_provider' => 'keystone.token.providers.uuid.Provider', + 'cache_dir' => '/var/lib/cache/keystone' + } + end + it { should contain_file('/var/lib/cache/keystone') } + end + end + + describe 'when configuring deprecated token_format as PKI with enable_pki_setup' do + let :params do + { + 'admin_token' => 'service_token', + 'token_format' => 'PKI', + } + end + it { should contain_exec('keystone-manage pki_setup').with( + :creates => '/etc/keystone/ssl/private/signing_key.pem' + ) } + it { should contain_file('/var/cache/keystone').with_ensure('directory') } + describe 'when overriding the cache dir' do + let :params do + { + 'admin_token' => 'service_token', + 'token_provider' => 'keystone.token.providers.pki.Provider', + 'cache_dir' => '/var/lib/cache/keystone' + } + end + it { should contain_file('/var/lib/cache/keystone') } + end + end + + describe 'when configuring deprecated token_format as PKI without enable_pki_setup' do + let :params do + { + 'admin_token' => 'service_token', + 'token_format' => 'PKI', + 'enable_pki_setup' => false + } + end + it { should_not contain_exec('keystone-manage pki_setup') } + it { should contain_file('/var/cache/keystone').with_ensure('directory') } + describe 'when overriding the cache dir' do + let :params do + { + 'admin_token' => 'service_token', + 'token_provider' => 'keystone.token.providers.pki.Provider', + 'cache_dir' => '/var/lib/cache/keystone' + } + end + it { should contain_file('/var/lib/cache/keystone') } + end + end + + end + + describe 'when configuring token expiration' do + let :params do + { + 'admin_token' => 'service_token', + 'token_expiration' => '42', + } + end + + it { should contain_keystone_config("token/expiration").with_value('42') } + end + + describe 'when not configuring token expiration' do + let :params do + { + 'admin_token' => 'service_token', + } + end + + it { should contain_keystone_config("token/expiration").with_value('3600') } + end + + describe 'configure memcache servers if set' do + let :params do + { + 'admin_token' => 'service_token', + 'memcache_servers' => [ 'SERVER1:11211', 'SERVER2:11211' ], + 'token_driver' => 'keystone.token.backends.memcache.Token' + } + end + + it { should contain_keystone_config("memcache/servers").with_value('SERVER1:11211,SERVER2:11211') } + it { should contain_package('python-memcache').with( + :name => 'python-memcache', + :ensure => 'present' + ) } + end + + describe 'do not configure memcache servers when not set' do + let :params do + default_params + end + + it { should contain_keystone_config("memcache/servers").with_ensure('absent') } + end + + describe 'raise error if memcache_servers is not an array' do + let :params do + { + 'admin_token' => 'service_token', + 'memcache_servers' => 'ANY_SERVER:11211' + } + end + + it { expect { should contain_class('keystone::params') }.to \ + raise_error(Puppet::Error, /is not an Array/) } + end + + describe 'with syslog disabled by default' do + let :params do + default_params + end + + it { should contain_keystone_config('DEFAULT/use_syslog').with_value(false) } + it { should_not contain_keystone_config('DEFAULT/syslog_log_facility') } + end + + describe 'with syslog enabled' do + let :params do + default_params.merge({ + :use_syslog => 'true', + }) + end + + it { should contain_keystone_config('DEFAULT/use_syslog').with_value(true) } + it { should contain_keystone_config('DEFAULT/syslog_log_facility').with_value('LOG_USER') } + end + + describe 'with syslog enabled and custom settings' do + let :params do + default_params.merge({ + :use_syslog => 'true', + :log_facility => 'LOG_LOCAL0' + }) + end + + it { should contain_keystone_config('DEFAULT/use_syslog').with_value(true) } + it { should contain_keystone_config('DEFAULT/syslog_log_facility').with_value('LOG_LOCAL0') } + end + + describe 'with log_file disabled by default' do + let :params do + default_params + end + it { should contain_keystone_config('DEFAULT/log_file').with_ensure('absent') } + end + + describe 'with log_file and log_dir enabled' do + let :params do + default_params.merge({ + :log_file => 'keystone.log', + :log_dir => '/var/lib/keystone' + }) + end + it { should contain_keystone_config('DEFAULT/log_file').with_value('keystone.log') } + it { should contain_keystone_config('DEFAULT/log_dir').with_value('/var/lib/keystone') } + end + + describe 'with log_file and log_dir disabled' do + let :params do + default_params.merge({ + :log_file => false, + :log_dir => false + }) + end + it { should contain_keystone_config('DEFAULT/log_file').with_ensure('absent') } + it { should contain_keystone_config('DEFAULT/log_dir').with_ensure('absent') } + end + + describe 'when configuring api binding with deprecated parameter' do + let :params do + default_params.merge({ + :bind_host => '10.0.0.2', + }) + end + it { should contain_keystone_config('DEFAULT/public_bind_host').with_value('10.0.0.2') } + it { should contain_keystone_config('DEFAULT/admin_bind_host').with_value('10.0.0.2') } + end + + describe 'when enabling SSL' do + let :params do + { + 'admin_token' => 'service_token', + 'enable_ssl' => true, + 'public_endpoint' => 'https://localhost:5000/v2.0/', + 'admin_endpoint' => 'https://localhost:35357/v2.0/', + } + end + it {should contain_keystone_config('ssl/enable').with_value(true)} + it {should contain_keystone_config('ssl/certfile').with_value('/etc/keystone/ssl/certs/keystone.pem')} + it {should contain_keystone_config('ssl/keyfile').with_value('/etc/keystone/ssl/private/keystonekey.pem')} + it {should contain_keystone_config('ssl/ca_certs').with_value('/etc/keystone/ssl/certs/ca.pem')} + it {should contain_keystone_config('ssl/ca_key').with_value('/etc/keystone/ssl/private/cakey.pem')} + it {should contain_keystone_config('ssl/cert_subject').with_value('/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost')} + it {should contain_keystone_config('DEFAULT/public_endpoint').with_value('https://localhost:5000/v2.0/')} + it {should contain_keystone_config('DEFAULT/admin_endpoint').with_value('https://localhost:35357/v2.0/')} + end + describe 'when disabling SSL' do + let :params do + { + 'admin_token' => 'service_token', + 'enable_ssl' => false, + } + end + it {should contain_keystone_config('ssl/enable').with_value(false)} + it {should contain_keystone_config('DEFAULT/public_endpoint').with_ensure('absent')} + it {should contain_keystone_config('DEFAULT/admin_endpoint').with_ensure('absent')} + end + describe 'not setting notification settings by default' do + let :params do + default_params + end + + it { should contain_keystone_config('DEFAULT/notification_driver').with_value(nil) } + it { should contain_keystone_config('DEFAULT/notification_topics').with_vaule(nil) } + it { should contain_keystone_config('DEFAULT/control_exchange').with_vaule(nil) } + end + + describe 'with RabbitMQ communication SSLed' do + let :params do + default_params.merge!({ + :rabbit_use_ssl => true, + :kombu_ssl_ca_certs => '/path/to/ssl/ca/certs', + :kombu_ssl_certfile => '/path/to/ssl/cert/file', + :kombu_ssl_keyfile => '/path/to/ssl/keyfile', + :kombu_ssl_version => 'SSLv3' + }) + end + + it do + should contain_keystone_config('DEFAULT/rabbit_use_ssl').with_value('true') + should contain_keystone_config('DEFAULT/kombu_ssl_ca_certs').with_value('/path/to/ssl/ca/certs') + should contain_keystone_config('DEFAULT/kombu_ssl_certfile').with_value('/path/to/ssl/cert/file') + should contain_keystone_config('DEFAULT/kombu_ssl_keyfile').with_value('/path/to/ssl/keyfile') + should contain_keystone_config('DEFAULT/kombu_ssl_version').with_value('SSLv3') + end + end + + describe 'with RabbitMQ communication not SSLed' do + let :params do + default_params.merge!({ + :rabbit_use_ssl => false, + :kombu_ssl_ca_certs => 'undef', + :kombu_ssl_certfile => 'undef', + :kombu_ssl_keyfile => 'undef', + :kombu_ssl_version => 'SSLv3' + }) + end + + it do + should contain_keystone_config('DEFAULT/rabbit_use_ssl').with_value('false') + should contain_keystone_config('DEFAULT/kombu_ssl_ca_certs').with_ensure('absent') + should contain_keystone_config('DEFAULT/kombu_ssl_certfile').with_ensure('absent') + should contain_keystone_config('DEFAULT/kombu_ssl_keyfile').with_ensure('absent') + should contain_keystone_config('DEFAULT/kombu_ssl_version').with_ensure('absent') + end + end + + describe 'setting notification settings' do + let :params do + default_params.merge({ + :notification_driver => 'keystone.openstack.common.notifier.rpc_notifier', + :notification_topics => 'notifications', + :control_exchange => 'keystone' + }) + end + + it { should contain_keystone_config('DEFAULT/notification_driver').with_value('keystone.openstack.common.notifier.rpc_notifier') } + it { should contain_keystone_config('DEFAULT/notification_topics').with_value('notifications') } + it { should contain_keystone_config('DEFAULT/control_exchange').with_value('keystone') } + end + + describe 'setting sql (default) catalog' do + let :params do + default_params + end + + it { should contain_keystone_config('catalog/driver').with_value('keystone.catalog.backends.sql.Catalog') } + end + + describe 'setting default template catalog' do + let :params do + { + :admin_token => 'service_token', + :catalog_type => 'template' + } + end + + it { should contain_keystone_config('catalog/driver').with_value('keystone.catalog.backends.templated.Catalog') } + it { should contain_keystone_config('catalog/template_file').with_value('/etc/keystone/default_catalog.templates') } + end + + describe 'with overridden validation_auth_url' do + let :params do + { + :admin_token => 'service_token', + :validate_service => true, + :validate_auth_url => 'http://some.host:35357/v2.0', + :admin_endpoint => 'http://some.host:35357' + } + end + + it { should contain_keystone_config('DEFAULT/admin_endpoint').with_value('http://some.host:35357') } + it { should contain_class('keystone::service').with( + 'validate' => true, + 'admin_endpoint' => 'http://some.host:35357/v2.0' + )} + end + + describe 'with service validation' do + let :params do + { + :admin_token => 'service_token', + :validate_service => true, + :admin_endpoint => 'http://some.host:35357' + } + end + + it { should contain_class('keystone::service').with( + 'validate' => true, + 'admin_endpoint' => 'http://some.host:35357' + )} + end + + describe 'setting another template catalog' do + let :params do + { + :admin_token => 'service_token', + :catalog_type => 'template', + :catalog_template_file => '/some/template_file' + } + end + + it { should contain_keystone_config('catalog/driver').with_value('keystone.catalog.backends.templated.Catalog') } + it { should contain_keystone_config('catalog/template_file').with_value('/some/template_file') } + end + + describe 'setting service_provider' do + let :facts do + global_facts.merge({ + :osfamily => 'RedHat', + :operatingsystemrelease => '6.0' + }) + end + + describe 'with default service_provider' do + let :params do + { 'admin_token' => 'service_token' } + end + + it { should contain_service('keystone').with( + :provider => nil + )} + end + + describe 'with overrided service_provider' do + let :params do + { + 'admin_token' => 'service_token', + 'service_provider' => 'pacemaker' + } + end + + it { should contain_service('keystone').with( + :provider => 'pacemaker' + )} + end + end +end diff --git a/keystone/spec/classes/keystone_wsgi_apache_spec.rb b/keystone/spec/classes/keystone_wsgi_apache_spec.rb new file mode 100644 index 000000000..5745d6b85 --- /dev/null +++ b/keystone/spec/classes/keystone_wsgi_apache_spec.rb @@ -0,0 +1,259 @@ +require 'spec_helper' + +describe 'keystone::wsgi::apache' do + + let :global_facts do + { + :processorcount => 42, + :concat_basedir => '/var/lib/puppet/concat', + :fqdn => 'some.host.tld' + } + end + + let :pre_condition do + [ + 'class { keystone: admin_token => "dummy", service_name => "httpd", enable_ssl => true }' + ] + end + + # concat::fragment { "${name}-wsgi": + # $filename = regsubst($name, ' ', '_', 'G') + # target => "${priority_real}-${filename}.conf", + # $safe_name = regsubst($name, '[/:\n]', '_', 'GM') + # $safe_target_name = regsubst($target, '[/:\n]', '_', 'GM') + # $concatdir = $concat::setup::concatdir + # $fragdir = "${concatdir}/${safe_target_name}" + # file { "${fragdir}/fragments/${order}_${safe_name}": + def get_concat_name(base_name) +# pp subject.resources + priority = 10 + order = 250 + base_dir = facts[:concat_basedir] + safe_name = base_name.gsub(/[\/:\n]/m, '_') + '-wsgi' + target = "#{priority}-#{base_name}.conf" + safe_target_name = target.gsub(/[\/:\n]/m, '_') + frag_dir = "#{base_dir}/#{safe_target_name}" + full_name = "#{frag_dir}/fragments/#{order}_#{safe_name}" + return full_name + end + + shared_examples_for 'apache serving keystone with mod_wsgi' do + it { should contain_service('httpd').with_name(platform_parameters[:httpd_service_name]) } + it { should contain_class('keystone::params') } + it { should contain_class('apache') } + it { should contain_class('apache::mod::wsgi') } + it { should contain_class('keystone::db::sync') } + + describe 'with default parameters' do + + it { should contain_file("#{platform_parameters[:wsgi_script_path]}").with( + 'ensure' => 'directory', + 'owner' => 'keystone', + 'group' => 'keystone', + 'require' => 'Package[httpd]' + )} + + it { should contain_file('keystone_wsgi_admin').with( + 'ensure' => 'file', + 'path' => "#{platform_parameters[:wsgi_script_path]}/admin", + 'source' => platform_parameters[:wsgi_script_source], + 'owner' => 'keystone', + 'group' => 'keystone', + 'mode' => '0644', + 'require' => ["File[#{platform_parameters[:wsgi_script_path]}]", "Package[keystone]"] + )} + + it { should contain_file('keystone_wsgi_main').with( + 'ensure' => 'file', + 'path' => "#{platform_parameters[:wsgi_script_path]}/main", + 'source' => platform_parameters[:wsgi_script_source], + 'owner' => 'keystone', + 'group' => 'keystone', + 'mode' => '0644', + 'require' => ["File[#{platform_parameters[:wsgi_script_path]}]", "Package[keystone]"] + )} + + it { should contain_apache__vhost('keystone_wsgi_admin').with( + 'servername' => 'some.host.tld', + 'ip' => nil, + 'port' => '35357', + 'docroot' => "#{platform_parameters[:wsgi_script_path]}", + 'docroot_owner' => 'keystone', + 'docroot_group' => 'keystone', + 'ssl' => 'true', + 'wsgi_daemon_process' => 'keystone_admin', + 'wsgi_process_group' => 'keystone_admin', + 'wsgi_script_aliases' => { '/' => "#{platform_parameters[:wsgi_script_path]}/admin" }, + 'require' => 'File[keystone_wsgi_admin]' + )} + + it { should contain_apache__vhost('keystone_wsgi_main').with( + 'servername' => 'some.host.tld', + 'ip' => nil, + 'port' => '5000', + 'docroot' => "#{platform_parameters[:wsgi_script_path]}", + 'docroot_owner' => 'keystone', + 'docroot_group' => 'keystone', + 'ssl' => 'true', + 'wsgi_daemon_process' => 'keystone_main', + 'wsgi_process_group' => 'keystone_main', + 'wsgi_script_aliases' => { '/' => "#{platform_parameters[:wsgi_script_path]}/main" }, + 'require' => 'File[keystone_wsgi_main]' + )} + it { should contain_file(get_concat_name('keystone_wsgi_main')).with_content( + /^ WSGIDaemonProcess keystone_main group=keystone processes=1 threads=#{facts[:processorcount]} user=keystone$/ + )} + it { should contain_file(get_concat_name('keystone_wsgi_admin')).with_content( + /^ WSGIDaemonProcess keystone_admin group=keystone processes=1 threads=#{facts[:processorcount]} user=keystone$/ + )} + it { should contain_file("#{platform_parameters[:httpd_ports_file]}") } + end + + describe 'when overriding parameters using different ports' do + let :params do + { + :servername => 'dummy.host', + :bind_host => '10.42.51.1', + :public_port => 12345, + :admin_port => 4142, + :ssl => false, + :workers => 37, + } + end + + it { should contain_apache__vhost('keystone_wsgi_admin').with( + 'servername' => 'dummy.host', + 'ip' => '10.42.51.1', + 'port' => '4142', + 'docroot' => "#{platform_parameters[:wsgi_script_path]}", + 'docroot_owner' => 'keystone', + 'docroot_group' => 'keystone', + 'ssl' => 'false', + 'wsgi_daemon_process' => 'keystone_admin', + 'wsgi_process_group' => 'keystone_admin', + 'wsgi_script_aliases' => { '/' => "#{platform_parameters[:wsgi_script_path]}/admin" }, + 'require' => 'File[keystone_wsgi_admin]' + )} + + it { should contain_apache__vhost('keystone_wsgi_main').with( + 'servername' => 'dummy.host', + 'ip' => '10.42.51.1', + 'port' => '12345', + 'docroot' => "#{platform_parameters[:wsgi_script_path]}", + 'docroot_owner' => 'keystone', + 'docroot_group' => 'keystone', + 'ssl' => 'false', + 'wsgi_daemon_process' => 'keystone_main', + 'wsgi_process_group' => 'keystone_main', + 'wsgi_script_aliases' => { '/' => "#{platform_parameters[:wsgi_script_path]}/main" }, + 'require' => 'File[keystone_wsgi_main]' + )} + it { should contain_file(get_concat_name('keystone_wsgi_main')).with_content( + /^ WSGIDaemonProcess keystone_main group=keystone processes=#{params[:workers]} threads=#{facts[:processorcount]} user=keystone$/ + )} + it { should contain_file(get_concat_name('keystone_wsgi_admin')).with_content( + /^ WSGIDaemonProcess keystone_admin group=keystone processes=#{params[:workers]} threads=#{facts[:processorcount]} user=keystone$/ + )} + it { should contain_file("#{platform_parameters[:httpd_ports_file]}") } + end + + describe 'when overriding parameters using same port' do + let :params do + { + :servername => 'dummy.host', + :public_port => 4242, + :admin_port => 4242, + :public_path => '/main/endpoint/', + :admin_path => '/admin/endpoint/', + :ssl => true, + :workers => 37, + } + end + + it { should_not contain_apache__vhost('keystone_wsgi_admin') } + + it { should contain_apache__vhost('keystone_wsgi_main').with( + 'servername' => 'dummy.host', + 'ip' => nil, + 'port' => '4242', + 'docroot' => "#{platform_parameters[:wsgi_script_path]}", + 'docroot_owner' => 'keystone', + 'docroot_group' => 'keystone', + 'ssl' => 'true', + 'wsgi_daemon_process' => 'keystone_main', + 'wsgi_process_group' => 'keystone_main', + 'wsgi_script_aliases' => { + '/main/endpoint' => "#{platform_parameters[:wsgi_script_path]}/main", + '/admin/endpoint' => "#{platform_parameters[:wsgi_script_path]}/admin" + }, + 'require' => 'File[keystone_wsgi_main]' + )} + it { should contain_file(get_concat_name('keystone_wsgi_main')).with_content( + /^ WSGIDaemonProcess keystone_main group=keystone processes=#{params[:workers]} threads=#{facts[:processorcount]} user=keystone$/ + )} + it do + expect_file = get_concat_name('keystone_wsgi_admin') + expect { + should contain_file(expect_file) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError, /expected that the catalogue would contain File\[#{expect_file}\]/) + end + end + + describe 'when overriding parameters using same port and same path' do + let :params do + { + :servername => 'dummy.host', + :public_port => 4242, + :admin_port => 4242, + :public_path => '/endpoint/', + :admin_path => '/endpoint/', + :ssl => true, + :workers => 37, + } + end + + it_raises 'a Puppet::Error', /When using the same port for public & private endpoints, public_path and admin_path should be different\./ + end + end + + context 'on RedHat platforms' do + let :facts do + global_facts.merge({ + :osfamily => 'RedHat', + :operatingsystemrelease => '6.0' + }) + end + + let :platform_parameters do + { + :httpd_service_name => 'httpd', + :httpd_ports_file => '/etc/httpd/conf/ports.conf', + :wsgi_script_path => '/var/www/cgi-bin/keystone', + :wsgi_script_source => '/usr/share/keystone/keystone.wsgi' + } + end + + it_configures 'apache serving keystone with mod_wsgi' + end + + context 'on Debian platforms' do + let :facts do + global_facts.merge({ + :osfamily => 'Debian', + :operatingsystem => 'Debian', + :operatingsystemrelease => '7.0' + }) + end + + let :platform_parameters do + { + :httpd_service_name => 'apache2', + :httpd_ports_file => '/etc/apache2/ports.conf', + :wsgi_script_path => '/usr/lib/cgi-bin/keystone', + :wsgi_script_source => '/usr/share/keystone/wsgi.py' + } + end + + it_configures 'apache serving keystone with mod_wsgi' + end +end diff --git a/keystone/spec/shared_examples.rb b/keystone/spec/shared_examples.rb new file mode 100644 index 000000000..d92156a36 --- /dev/null +++ b/keystone/spec/shared_examples.rb @@ -0,0 +1,5 @@ +shared_examples_for "a Puppet::Error" do |description| + it "with message matching #{description.inspect}" do + expect { should have_class_count(1) }.to raise_error(Puppet::Error, description) + end +end diff --git a/keystone/spec/spec.opts b/keystone/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/keystone/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/keystone/spec/spec_helper.rb b/keystone/spec/spec_helper.rb new file mode 100644 index 000000000..92560eb2b --- /dev/null +++ b/keystone/spec/spec_helper.rb @@ -0,0 +1,8 @@ +require 'puppetlabs_spec_helper/module_spec_helper' +require 'shared_examples' + +RSpec.configure do |c| + c.alias_it_should_behave_like_to :it_configures, 'configures' + c.alias_it_should_behave_like_to :it_raises, 'raises' +end + diff --git a/keystone/spec/unit/provider/keystone_endpoint/keystone_spec.rb b/keystone/spec/unit/provider/keystone_endpoint/keystone_spec.rb new file mode 100644 index 000000000..41ccefc23 --- /dev/null +++ b/keystone/spec/unit/provider/keystone_endpoint/keystone_spec.rb @@ -0,0 +1,74 @@ +require 'puppet' +require 'spec_helper' +require 'puppet/provider/keystone_endpoint/keystone' + + +describe Puppet::Type.type(:keystone_endpoint).provider(:keystone) do + + let :resource do + Puppet::Type::Keystone_endpoint.new( + :provider => :keystone, + :name => 'region/foo', + :ensure => :present, + :public_url => 'public_url', + :internal_url => 'internal_url', + :admin_url => 'admin_url' + ) + end + + let :provider do + described_class.new(resource) + end + + before :each do + # keystone endpoint-list + described_class.stubs(:list_keystone_objects).with('endpoint', [5,6]).returns([ + ['endpoint-id', 'region', 'public_url', 'internal_url', 'admin_url', 4] + ]) + # keystone service-list + described_class.stubs(:list_keystone_objects).with('service', 4).returns([ + [4, 'foo', 'type', 'description'] + ]) + described_class.stubs(:get_keystone_object).with('service', 4, 'name').returns('foo') + described_class.prefetch('region/foo' => resource) + end + + after :each do + described_class.prefetch({}) + end + + describe "self.instances" do + it "should have an instances method" do + provider.class.should respond_to(:instances) + end + + it "should list instances" do + endpoints = described_class.instances + endpoints.size.should == 1 + endpoints.map {|provider| provider.name} == ['region/foo'] + end + end + + describe '#create' do + it 'should call endpoint-create' do + provider.expects(:auth_keystone).with( + 'endpoint-create', '--service-id', 4, includes( + '--publicurl', 'public_url', '--internalurl', 'internal_url', + '--region', 'region') + ) + provider.create + end + end + + describe '#flush' do + it 'should delete and create the endpoint once when any url gets updated' do + provider.expects(:destroy).times(1) + provider.expects(:create).times(1) + + provider.public_url=('new-public_url') + provider.internal_url=('new-internal_url') + provider.admin_url=('new-admin_url') + provider.flush + end + end +end diff --git a/keystone/spec/unit/provider/keystone_spec.rb b/keystone/spec/unit/provider/keystone_spec.rb new file mode 100644 index 000000000..3996a34ed --- /dev/null +++ b/keystone/spec/unit/provider/keystone_spec.rb @@ -0,0 +1,164 @@ +require 'puppet' +require 'spec_helper' +require 'puppet/provider/keystone' +require 'tempfile' + + +klass = Puppet::Provider::Keystone + +describe Puppet::Provider::Keystone do + + after :each do + klass.reset + end + + + describe 'when retrieving the security token' do + + it 'should fail if there is no keystone config file' do + ini_file = Puppet::Util::IniConfig::File.new + t = Tempfile.new('foo') + path = t.path + t.unlink + ini_file.read(path) + expect do + klass.get_admin_token + end.to raise_error(Puppet::Error, /Keystone types will not work/) + end + + it 'should fail if the keystone config file does not have a DEFAULT section' do + mock = {} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + expect do + + klass.get_admin_token + end.to raise_error(Puppet::Error, /Keystone types will not work/) + end + + it 'should fail if the keystone config file does not contain an admin token' do + mock = {'DEFAULT' => {'not_a_token' => 'foo'}} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + expect do + klass.get_admin_token + end.to raise_error(Puppet::Error, /Keystone types will not work/) + end + + it 'should parse the admin token if it is in the config file' do + mock = {'DEFAULT' => {'admin_token' => 'foo'}} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.get_admin_token.should == 'foo' + end + + it 'should use the specified bind_host in the admin endpoint' do + mock = {'DEFAULT' => {'admin_bind_host' => '192.168.56.210', 'admin_port' => '35357' }} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.get_admin_endpoint.should == 'http://192.168.56.210:35357/v2.0/' + end + + it 'should use localhost in the admin endpoint if bind_host is 0.0.0.0' do + mock = {'DEFAULT' => { 'admin_bind_host' => '0.0.0.0', 'admin_port' => '35357' }} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.get_admin_endpoint.should == 'http://127.0.0.1:35357/v2.0/' + end + + it 'should use localhost in the admin endpoint if bind_host is unspecified' do + mock = {'DEFAULT' => { 'admin_port' => '35357' }} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.get_admin_endpoint.should == 'http://127.0.0.1:35357/v2.0/' + end + + it 'should use https if ssl is enabled' do + mock = {'DEFAULT' => {'admin_bind_host' => '192.168.56.210', 'admin_port' => '35357' }, 'ssl' => {'enable' => 'True'}} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.get_admin_endpoint.should == 'https://192.168.56.210:35357/v2.0/' + end + + it 'should use http if ssl is disabled' do + mock = {'DEFAULT' => {'admin_bind_host' => '192.168.56.210', 'admin_port' => '35357' }, 'ssl' => {'enable' => 'False'}} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.get_admin_endpoint.should == 'http://192.168.56.210:35357/v2.0/' + end + + it 'should use the defined admin_endpoint if available' do + mock = {'DEFAULT' => {'admin_endpoint' => 'https://keystone.example.com' }, 'ssl' => {'enable' => 'False'}} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.get_admin_endpoint.should == 'https://keystone.example.com/v2.0/' + end + + it 'should handle an admin_endpoint with a trailing slash' do + mock = {'DEFAULT' => {'admin_endpoint' => 'https://keystone.example.com/' }, 'ssl' => {'enable' => 'False'}} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.get_admin_endpoint.should == 'https://keystone.example.com/v2.0/' + end + + describe 'when testing keystone connection retries' do + + ['(HTTP 400)', + '[Errno 111] Connection refused', + '503 Service Unavailable', + 'Max retries exceeded', + 'HTTP Unable to establish connection', + 'Unable to establish connection to http://127.0.0.1:35357/v2.0/OS-KSADM/roles' + ].reverse.each do |valid_message| + it "should retry when keystone is not ready with error #{valid_message}" do + mock = {'DEFAULT' => {'admin_token' => 'foo'}} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.expects(:sleep).with(10).returns(nil) + klass.expects(:keystone).twice.with('--os-endpoint', 'http://127.0.0.1:35357/v2.0/', ['test_retries']).raises(Exception, valid_message).then.returns('') + klass.auth_keystone('test_retries') + end + end + end + + end + + describe 'when keystone cli has warnings' do + it "should remove errors from results" do + mock = {'DEFAULT' => {'admin_token' => 'foo'}} + Puppet::Util::IniConfig::File.expects(:new).returns(mock) + mock.expects(:read).with('/etc/keystone/keystone.conf') + klass.expects( + :keystone + ).with( + '--os-endpoint', + 'http://127.0.0.1:35357/v2.0/', + ['test_retries'] + ).returns("WARNING\n+-+-+\nWARNING") + klass.auth_keystone('test_retries').should == "+-+-+\nWARNING" + end + end + + describe 'when parsing keystone objects' do + it 'should parse valid output into a hash' do + data = <<-EOT ++-------------+----------------------------------+ +| Property | Value | ++-------------+----------------------------------+ +| description | default tenant | +| enabled | True | +| id | b71040f47e144399b7f10182918b5e2f | +| name | demo | ++-------------+----------------------------------+ + EOT + expected = { + 'description' => 'default tenant', + 'enabled' => 'True', + 'id' => 'b71040f47e144399b7f10182918b5e2f', + 'name' => 'demo' + } + klass.parse_keystone_object(data).should == expected + end + end + +end diff --git a/keystone/spec/unit/provider/keystone_tenant/keystone_spec.rb b/keystone/spec/unit/provider/keystone_tenant/keystone_spec.rb new file mode 100644 index 000000000..8943906dc --- /dev/null +++ b/keystone/spec/unit/provider/keystone_tenant/keystone_spec.rb @@ -0,0 +1,55 @@ +require 'puppet' +require 'spec_helper' +require 'puppet/provider/keystone_tenant/keystone' + +provider_class = Puppet::Type.type(:keystone_tenant).provider(:keystone) + +describe provider_class do + + describe 'when updating a tenant' do + + let :tenant_name do + 'foo' + end + + let :tenant_attrs do + { + :name => tenant_name, + :description => '', + :ensure => 'present', + :enabled => 'True', + } + end + + let :tenant_hash do + { tenant_name => { + :id => 'id', + :name => tenant_name, + :description => '', + :ensure => 'present', + :enabled => 'True', + } + } + end + + let :resource do + Puppet::Type::Keystone_tenant.new(tenant_attrs) + end + + let :provider do + provider_class.new(resource) + end + + before :each do + provider_class.expects(:build_tenant_hash).returns(tenant_hash) + end + + it 'should call tenant-update to set enabled' do + provider.expects(:auth_keystone).with('tenant-update', + '--enabled', + 'False', + 'id') + provider.enabled=('False') + end + end +end diff --git a/keystone/spec/unit/provider/keystone_user/keystone_spec.rb b/keystone/spec/unit/provider/keystone_user/keystone_spec.rb new file mode 100644 index 000000000..8693985d4 --- /dev/null +++ b/keystone/spec/unit/provider/keystone_user/keystone_spec.rb @@ -0,0 +1,53 @@ +require 'puppet' +require 'spec_helper' +require 'puppet/provider/keystone_user/keystone' + +provider_class = Puppet::Type.type(:keystone_user).provider(:keystone) + +describe provider_class do + + describe 'when updating a user' do + let :resource do + Puppet::Type::Keystone_user.new( + { + :name => 'foo', + :ensure => 'present', + :enabled => 'True', + :tenant => 'foo2', + :email => 'foo@foo.com', + :password => 'passwd' + } + ) + end + + let :provider do + provider_class.new(resource) + end + + before :each do + provider_class.expects(:build_user_hash).returns( + 'foo' => {:id => 'id', :name => 'foo', :tenant => 'foo2', :password => 'passwd'} + ) + end + + after :each do + # reset global state + provider_class.prefetch(nil) + end + + it 'should call user-password-update to change password' do + provider.expects(:auth_keystone).with('user-password-update', '--pass', 'newpassword', 'id') + provider.password=('newpassword') + end + + it 'should call user-update to change email' do + provider.expects(:auth_keystone).with('user-update', '--email', 'bar@bar.com', 'id') + provider.email=('bar@bar.com') + end + + it 'should call user-update to set email to blank' do + provider.expects(:auth_keystone).with('user-update', '--email', '', 'id') + provider.email=('') + end + end +end diff --git a/keystone/spec/unit/provider/keystone_user_role/keystone_spec.rb b/keystone/spec/unit/provider/keystone_user_role/keystone_spec.rb new file mode 100644 index 000000000..04bac7241 --- /dev/null +++ b/keystone/spec/unit/provider/keystone_user_role/keystone_spec.rb @@ -0,0 +1,40 @@ +require 'puppet' +require 'spec_helper' +require 'puppet/provider/keystone_user_role/keystone' + +provider_class = Puppet::Type.type(:keystone_user_role).provider(:keystone) + +describe provider_class do + + describe '#get_user_and_tenant' do + + let :user do + 'username@example.org' + end + + let :tenant do + 'test' + end + + let :resource do + Puppet::Type::Keystone_user_role.new( + { + :name => "#{user}@#{tenant}", + :roles => [ '_member_' ], + } + ) + end + + let :provider do + provider_class.new(resource) + end + + before :each do + provider_class.expects(:get_user_and_tenant).with(user,tenant).returns([user,tenant]) + end + + it 'should handle an email address as username' do + provider.get_user_and_tenant.should == [ user, tenant ] + end + end +end diff --git a/keystone/spec/unit/type/keystone_endpoint_spec.rb b/keystone/spec/unit/type/keystone_endpoint_spec.rb new file mode 100644 index 000000000..a3667c546 --- /dev/null +++ b/keystone/spec/unit/type/keystone_endpoint_spec.rb @@ -0,0 +1,9 @@ +describe Puppet::Type.type(:keystone_endpoint) do + + it 'should fail when the namevar does not contain a region' do + expect do + Puppet::Type.type(:keystone_endpoint).new(:name => 'foo') + end.to raise_error(Puppet::Error, /Invalid value/) + end + +end diff --git a/keystone/tests/site.pp b/keystone/tests/site.pp new file mode 100644 index 000000000..ddd335590 --- /dev/null +++ b/keystone/tests/site.pp @@ -0,0 +1,68 @@ +Exec { logoutput => 'on_failure' } + +package { 'curl': ensure => present } + +# example of how to build a single node +# keystone instance backed by sqlite +# with all of the default admin roles +node keystone_sqlite { + class { 'keystone': + verbose => true, + debug => true, + catalog_type => 'sql', + admin_token => 'admin_token', + } + class { 'keystone::roles::admin': + email => 'example@abc.com', + password => 'ChangeMe', + } + class { 'keystone::endpoint': + public_url => "http://${::fqdn}:5000/", + admin_url => "http://${::fqdn}:35357/", + } +} + +node keystone_mysql { + class { 'mysql::server': } + class { 'keystone::db::mysql': + password => 'keystone', + } + class { 'keystone': + verbose => true, + debug => true, + sql_connection => 'mysql://keystone:keystone@127.0.0.1/keystone', + catalog_type => 'sql', + admin_token => 'admin_token', + } + class { 'keystone::roles::admin': + email => 'test@puppetlabs.com', + password => 'ChangeMe', + } +} + + +# keystone with mysql on another node +node keystone { + class { 'keystone': + verbose => true, + debug => true, + sql_connection => 'mysql://keystone:password@127.0.0.1/keystone', + catalog_type => 'sql', + admin_token => 'admin_token', + } + class { 'keystone::db::mysql': + password => 'keystone', + } + class { 'keystone::roles::admin': + email => 'example@abc.com', + password => 'ChangeMe', + } + class { 'keystone::endpoint': + public_url => "http://${::fqdn}:5000/", + admin_url => "http://${::fqdn}:35357/", + } +} + +node default { + fail("could not find a matching node entry for ${clientcert}") +} diff --git a/memcached b/memcached deleted file mode 160000 index 49dbf102f..000000000 --- a/memcached +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 49dbf102fb6eee90297b2ed6a1fa463a8c5ccee7 diff --git a/memcached/.fixtures.yml b/memcached/.fixtures.yml new file mode 100644 index 000000000..ff6d34112 --- /dev/null +++ b/memcached/.fixtures.yml @@ -0,0 +1,3 @@ +fixtures: + symlinks: + "memcached": "#{source_dir}" diff --git a/memcached/.gemfile b/memcached/.gemfile new file mode 100644 index 000000000..9aad840c0 --- /dev/null +++ b/memcached/.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/memcached/.gitignore b/memcached/.gitignore new file mode 100644 index 000000000..37e8aed56 --- /dev/null +++ b/memcached/.gitignore @@ -0,0 +1,3 @@ +pkg/ +*.swp +/metadata.json \ No newline at end of file diff --git a/memcached/.travis.yml b/memcached/.travis.yml new file mode 100644 index 000000000..37e322a76 --- /dev/null +++ b/memcached/.travis.yml @@ -0,0 +1,17 @@ +language: ruby +rvm: + - 1.8.7 +before_script: +after_script: +script: "rake spec" +branches: + only: + - master +env: + - PUPPET_VERSION=2.7.13 + - PUPPET_VERSION=2.7.6 + - PUPPET_VERSION=2.6.9 + - PUPPET_VERSION=3.0.0 +notifications: + email: false +gemfile: .gemfile diff --git a/memcached/LICENSE b/memcached/LICENSE new file mode 100644 index 000000000..c46e2ee1d --- /dev/null +++ b/memcached/LICENSE @@ -0,0 +1,13 @@ + Copyright 2011 Steffen Zieger + + 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/memcached/Modulefile b/memcached/Modulefile new file mode 100644 index 000000000..3f339a9c6 --- /dev/null +++ b/memcached/Modulefile @@ -0,0 +1,8 @@ +name 'saz-memcached' +version '2.0.4' +source 'git://github.com/saz/puppet-memcached.git' +author 'saz' +license 'Apache License, Version 2.0' +summary 'UNKNOWN' +description 'Manage memcached via Puppet' +project_page 'https://github.com/saz/puppet-memcached' diff --git a/memcached/README-DEVELOPER b/memcached/README-DEVELOPER new file mode 100644 index 000000000..e6c4dab93 --- /dev/null +++ b/memcached/README-DEVELOPER @@ -0,0 +1,9 @@ +In order to run tests: + - puppet and facter must be installed and available in Ruby's LOADPATH + - the latest revision of rspec-puppet must be installed + - rake, and rspec2 must be install + + - the name of the module directory needs to be memcached + +to run all tests: + rake spec diff --git a/memcached/README.md b/memcached/README.md new file mode 100644 index 000000000..01bded1b5 --- /dev/null +++ b/memcached/README.md @@ -0,0 +1,39 @@ +# puppet-memcached + +[![Build Status](https://secure.travis-ci.org/saz/puppet-memcached.png)](http://travis-ci.org/saz/puppet-memcached) + +Manage memcached via Puppet + +## How to use + +### Use roughly 90% of memory + +```ruby + class { 'memcached': } +``` + +### Set a fixed memory limit in MB + +```ruby + class { 'memcached': + max_memory => 2048 + } +``` + +### Use 12% of available memory + +```ruby + class { 'memcached': + max_memory => '12%' + } +``` + +### Other class parameters + +* $logfile = '/var/log/memcached.log' +* $listen_ip = '0.0.0.0' +* $tcp_port = 11211 +* $udp_port = 11211 +* $user = '' (OS specific setting, see params.pp) +* $max_connections = 8192 +* $lock_memory = false (WARNING: good if used intelligently, google for -k key) diff --git a/memcached/Rakefile b/memcached/Rakefile new file mode 100644 index 000000000..cd3d37995 --- /dev/null +++ b/memcached/Rakefile @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/rake_tasks' diff --git a/memcached/lib/puppet/parser/functions/memcached_max_memory.rb b/memcached/lib/puppet/parser/functions/memcached_max_memory.rb new file mode 100644 index 000000000..fac25fdb3 --- /dev/null +++ b/memcached/lib/puppet/parser/functions/memcached_max_memory.rb @@ -0,0 +1,38 @@ +module Puppet::Parser::Functions + newfunction(:memcached_max_memory, :type => :rvalue, :doc => <<-EOS + Calculate max_memory size from fact 'memsize' and passed argument. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "memcached_max_memory(): " + + "Wrong number of arguments given " + + "(#{arguments.size} for 1)") if arguments.size != 1 + + arg = arguments[0] + memsize = lookupvar('memorysize') + + if arg and !arg.to_s.end_with?('%') + result_in_mb = arg.to_i + else + max_memory_percent = arg ? arg : '95%' + + # Taken from puppetlabs-stdlib to_bytes() function + value,prefix = */([0-9.e+-]*)\s*([^bB]?)/.match(memsize)[1,2] + + value = value.to_f + case prefix + when '' then value = value + when 'k' then value *= (1<<10) + when 'M' then value *= (1<<20) + when 'G' then value *= (1<<30) + when 'T' then value *= (1<<40) + when 'E' then value *= (1<<50) + else raise Puppet::ParseError, "memcached_max_memory(): Unknown prefix #{prefix}" + end + value = value.to_i + result_in_mb = ( (value / (1 << 20) ) * (max_memory_percent.to_f / 100.0) ).floor + end + + return result_in_mb + end +end diff --git a/memcached/manifests/init.pp b/memcached/manifests/init.pp new file mode 100644 index 000000000..8d1ca1540 --- /dev/null +++ b/memcached/manifests/init.pp @@ -0,0 +1,34 @@ +class memcached( + $package_ensure = 'present', + $logfile = '/var/log/memcached.log', + $max_memory = false, + $lock_memory = false, + $listen_ip = '0.0.0.0', + $tcp_port = 11211, + $udp_port = 11211, + $user = $::memcached::params::user, + $max_connections = '8192', + $verbosity = undef, + $unix_socket = undef +) inherits memcached::params { + + package { $memcached::params::package_name: + ensure => $package_ensure, + } + + file { $memcached::params::config_file: + owner => 'root', + group => 'root', + mode => '0644', + content => template($memcached::params::config_tmpl), + require => Package[$memcached::params::package_name], + } + + service { $memcached::params::service_name: + ensure => running, + enable => true, + hasrestart => true, + hasstatus => false, + subscribe => File[$memcached::params::config_file], + } +} diff --git a/memcached/manifests/params.pp b/memcached/manifests/params.pp new file mode 100644 index 000000000..9c1ca4710 --- /dev/null +++ b/memcached/manifests/params.pp @@ -0,0 +1,21 @@ +class memcached::params { + case $::osfamily { + 'Debian': { + $package_name = 'memcached' + $service_name = 'memcached' + $config_file = '/etc/memcached.conf' + $config_tmpl = "$module_name/memcached.conf.erb" + $user = 'nobody' + } + 'RedHat': { + $package_name = 'memcached' + $service_name = 'memcached' + $config_file = '/etc/sysconfig/memcached' + $config_tmpl = "$module_name/memcached_sysconfig.erb" + $user = 'memcached' + } + default: { + fail("Unsupported platform: ${::osfamily}") + } + } +} diff --git a/memcached/spec/classes/memcached_spec.rb b/memcached/spec/classes/memcached_spec.rb new file mode 100644 index 000000000..d39701d16 --- /dev/null +++ b/memcached/spec/classes/memcached_spec.rb @@ -0,0 +1,126 @@ +require 'spec_helper' +describe 'memcached' do + + let :default_params do + { + :package_ensure => 'present', + :logfile => '/var/log/memcached.log', + :max_memory => false, + :lock_memory => false, + :listen_ip => '0.0.0.0', + :tcp_port => '11211', + :udp_port => '11211', + :user => 'nobody', + :max_connections => '8192' + } + end + + [ {}, + { + :package_ensure => 'latest', + :logfile => '/var/log/memcached.log', + :max_memory => '2', + :lock_memory => true, + :listen_ip => '127.0.0.1', + :tcp_port => '11212', + :udp_port => '11213', + :user => 'somebdy', + :max_connections => '8193', + :verbosity => 'vvv' + }, + { + :package_ensure => 'present', + :logfile => '/var/log/memcached.log', + :max_memory => '20%', + :lock_memory => false, + :listen_ip => '127.0.0.1', + :tcp_port => '11212', + :udp_port => '11213', + :user => 'somebdy', + :max_connections => '8193', + :verbosity => 'vvv' + } + ].each do |param_set| + describe "when #{param_set == {} ? "using default" : "specifying"} class parameters" do + + let :param_hash do + default_params.merge(param_set) + end + + let :params do + param_set + end + + ['Debian'].each do |osfamily| + + let :facts do + { + :osfamily => osfamily, + :memorysize => '1000 MB', + :processorcount => '1', + } + end + + describe "on supported osfamily: #{osfamily}" do + + it { should contain_class('memcached::params') } + + it { should contain_package('memcached').with_ensure(param_hash[:package_ensure]) } + + it { should contain_file('/etc/memcached.conf').with( + 'owner' => 'root', + 'group' => 'root' + )} + + it { should contain_service('memcached').with( + 'ensure' => 'running', + 'enable' => true, + 'hasrestart' => true, + 'hasstatus' => false + )} + + it 'should compile the template based on the class parameters' do + content = param_value( + subject, + 'file', + '/etc/memcached.conf', + 'content' + ) + expected_lines = [ + "logfile #{param_hash[:logfile]}", + "-l #{param_hash[:listen_ip]}", + "-p #{param_hash[:tcp_port]}", + "-U #{param_hash[:udp_port]}", + "-u #{param_hash[:user]}", + "-c #{param_hash[:max_connections]}", + "-t #{facts[:processorcount]}" + ] + if(param_hash[:max_memory]) + if(param_hash[:max_memory].end_with?('%')) + expected_lines.push("-m 200") + else + expected_lines.push("-m #{param_hash[:max_memory]}") + end + else + expected_lines.push("-m 950") + end + if(param_hash[:lock_memory]) + expected_lines.push("-k") + end + if(param_hash[:verbosity]) + expected_lines.push("-vvv") + end + (content.split("\n") & expected_lines).should =~ expected_lines + end + end + end + ['Redhat'].each do |osfamily| + describe 'on supported platform' do + it 'should fail' do + + end + end + end + end + end +end diff --git a/memcached/spec/spec.opts b/memcached/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/memcached/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/memcached/spec/spec_helper.rb b/memcached/spec/spec_helper.rb new file mode 100644 index 000000000..2c6f56649 --- /dev/null +++ b/memcached/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/memcached/templates/memcached.conf.erb b/memcached/templates/memcached.conf.erb new file mode 100644 index 000000000..03344d5da --- /dev/null +++ b/memcached/templates/memcached.conf.erb @@ -0,0 +1,47 @@ +# File managed by puppet + +# Run memcached as a daemon. +-d + +# pidfile +-P /var/run/memcached.pid + +# Log memcached's output +logfile <%= logfile -%> + +<% if @verbosity -%> +# Verbosity +-<%= verbosity %> +<% end -%> + +# Use MB memory max to use for object storage. +<% Puppet::Parser::Functions.function('memcached_max_memory') -%> +-m <%= scope.function_memcached_max_memory([max_memory]) %> + +<% if @lock_memory -%> +# Lock down all paged memory. There is a limit on how much memory you may lock. +-k +<% end -%> + +<% if @unix_socket -%> +# UNIX socket path to listen on +-s <%= unix_socket %> +<% else -%> +# IP to listen on +-l <%= listen_ip %> + +# TCP port to listen on +-p <%= tcp_port %> + +# UDP port to listen on +-U <%= udp_port %> +<% end -%> + +# Run daemon as user +-u <%= user %> + +# Limit the number of simultaneous incoming connections. +-c <%= max_connections %> + +# Number of threads to use to process incoming requests. +-t <%= processorcount %> diff --git a/memcached/templates/memcached_sysconfig.erb b/memcached/templates/memcached_sysconfig.erb new file mode 100644 index 000000000..3c980309b --- /dev/null +++ b/memcached/templates/memcached_sysconfig.erb @@ -0,0 +1,21 @@ +PORT="<%= tcp_port %>" +USER="<%= user %>" +MAXCONN="<%= max_connections %>" +<% Puppet::Parser::Functions.function('memcached_max_memory') -%> +CACHESIZE="<%= scope.function_memcached_max_memory([max_memory]) %>" +OPTIONS="<% +result = [] +if @verbosity + result << '-' + verbosity +end +if @lock_memory + result << '-k' +end +if @listen_ip + result << '-l ' + listen_ip +end +if @udp_port + result << '-U ' + udp_port +end +result << '-t ' + processorcount +-%><%= result.join(' ') -%>" diff --git a/memcached/tests/init.pp b/memcached/tests/init.pp new file mode 100644 index 000000000..22eecc327 --- /dev/null +++ b/memcached/tests/init.pp @@ -0,0 +1 @@ +include memcached diff --git a/module-data b/module-data deleted file mode 160000 index 159fc5e0e..000000000 --- a/module-data +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 159fc5e0e21ce9df96c777f0064b5eca88e29cae diff --git a/module-data/Modulefile b/module-data/Modulefile new file mode 100644 index 000000000..5c05ad258 --- /dev/null +++ b/module-data/Modulefile @@ -0,0 +1,6 @@ +name 'ripienaar-module_data' +version '0.0.3' +description 'A hiera backend to allow the use of data while writing sharable modules' +project_page 'https://github.com/ripienaar/puppet-module-data' +license 'ASL 2.0' +author 'R.I.Pienaar ' diff --git a/module-data/README.md b/module-data/README.md new file mode 100644 index 000000000..d1409ecb9 --- /dev/null +++ b/module-data/README.md @@ -0,0 +1,61 @@ +What? +===== + +While hiera does a decent job of separating code and data for users +it is quite difficult for module authors to use hiera to create reusable +modules. This is because the puppet backend is optional and even when +it is loaded the module author cannot influence the hierarchy. + +With this commit we add a new module_data backend that loads data from +within a module and allow the module author to set a hierarchy for this +data. + +The goal of this backend is to allow module authors to specify true +module default data in their modules but still allow users to override +the data using the standard method - especially useful with the puppet 3 +hiera integration. + +This backend is always loaded as the least important tier in the +hierarchy - unless a user choose to put it somewhere specific, but this +backend will always be enabled. + +Given a module layout: + + your_module + ├── data + │ ├── hiera.yaml + │ └── osfamily + │ ├── Debian.yaml + │ └── RedHat.yaml + └── manifests + └── init.pp + +The hiera.yaml is optional in this example it would be: + + --- + :hierarchy: + - osfamily/%{::osfamily} + - common + +But when no hiera.yaml exist in the module, the default would be: + + --- + :hierarchy: + - common + +The data directory is then a standard Hiera data store. + +Status? +------- + +This is but a first stab at turning my old pull request for ticket 16856 +into a standalone module that any > 3.0.0 Puppet user can depend on to +get this essential feature. + +Some more testing is needed, sanity checking for support versions etc so +consider this a early feedback-saught release + +Contact? +-------- + +R.I.Pienaar / rip@devco.net / @ripienaar / http://devco.net diff --git a/module-data/lib/hiera/backend/module_data_backend.rb b/module-data/lib/hiera/backend/module_data_backend.rb new file mode 100644 index 000000000..5e8ff3a37 --- /dev/null +++ b/module-data/lib/hiera/backend/module_data_backend.rb @@ -0,0 +1,91 @@ +class Hiera + module Backend + class Module_data_backend + def initialize(cache=nil) + require 'yaml' + require 'hiera/filecache' + + Hiera.debug("Hiera Module Data backend starting") + + @cache = cache || Filecache.new + end + + def load_module_config(module_name, environment) + default_config = {:hierarchy => ["common"]} + + mod = Puppet::Module.find(module_name, environment) + + return default_config unless mod + + path = mod.path + module_config = File.join(path, "data", "hiera.yaml") + config = {} + + if File.exist?(module_config) + Hiera.debug("Reading config from %s file" % module_config) + config = load_data(module_config) + end + + config["path"] = path + + default_config.merge(config) + end + + def load_data(path) + return {} unless File.exist?(path) + + @cache.read(path, Hash, {}) do |data| + YAML.load(data) + end + end + + def lookup(key, scope, order_override, resolution_type) + answer = nil + + Hiera.debug("Looking up %s in Module Data backend" % key) + + unless scope["module_name"] + Hiera.debug("Skipping Module Data backend as this does not look like a module") + return answer + end + + config = load_module_config(scope["module_name"], scope["environment"]) + + unless config["path"] + Hiera.debug("Could not find a path to the module '%s' in environment '%s'" % [scope["module_name"], scope["environment"]]) + return answer + end + + config[:hierarchy].each do |source| + source = File.join(config["path"], "data", "%s.yaml" % Backend.parse_string(source, scope)) + + Hiera.debug("Looking for data in source %s" % source) + data = load_data(source) + + raise("Data loaded from %s should be a hash but got %s" % [source, data.class]) unless data.is_a?(Hash) + + next if data.empty? + next unless data.include?(key) + + found = data[key] + + case resolution_type + when :array + raise("Hiera type mismatch: expected Array or String and got %s" % found.class) unless [Array, String].include?(found.class) + answer ||= [] + answer << Backend.parse_answer(found, scope) + + when :hash + raise("Hiera type mismatch: expected Hash and got %s" % found.class) unless found.is_a?(Hash) + answer = Backend.parse_answer(found, scope).merge(answer || {}) + else + answer = Backend.parse_answer(found, scope) + break + end + end + + return answer + end + end + end +end diff --git a/module-data/lib/puppet/indirector/data_binding/hiera.rb b/module-data/lib/puppet/indirector/data_binding/hiera.rb new file mode 100644 index 000000000..df8c8ba6b --- /dev/null +++ b/module-data/lib/puppet/indirector/data_binding/hiera.rb @@ -0,0 +1,75 @@ +require "hiera" +require "hiera/config" +require "hiera/scope" + +begin + require 'puppet/indirector/hiera' +rescue LoadError => e + begin + require "puppet/indirector/code" + rescue LoadError => e + $stderr.puts "Couldn't require either of puppet/indirector/{hiera,code}!" + end +end + + +class Hiera::Config + class << self + alias :old_load :load unless respond_to?(:old_load) + + def load(source) + old_load(source) + + @config[:backends] << "module_data" unless @config[:backends].include?("module_data") + + @config + end + end +end + +class Puppet::DataBinding::Hiera < Puppet::Indirector::Code + desc "Retrieve data using Hiera." + + def initialize(*args) + if ! Puppet.features.hiera? + raise "Hiera terminus not supported without hiera library" + end + super + end + + if defined?(::Psych::SyntaxError) + DataBindingExceptions = [::StandardError, ::Psych::SyntaxError] + else + DataBindingExceptions = [::StandardError] + end + + def find(request) + hiera.lookup(request.key, nil, Hiera::Scope.new(request.options[:variables]), nil, nil) + rescue *DataBindingExceptions => detail + raise Puppet::DataBinding::LookupError.new(detail.message, detail) + end + + private + + def self.hiera_config + hiera_config = Puppet.settings[:hiera_config] + config = {} + + if File.exist?(hiera_config) + config = Hiera::Config.load(hiera_config) + else + Puppet.warning "Config file #{hiera_config} not found, using Hiera defaults" + end + + config[:logger] = 'puppet' + config + end + + def self.hiera + @hiera ||= Hiera.new(:config => hiera_config) + end + + def hiera + self.class.hiera + end +end diff --git a/mongodb b/mongodb deleted file mode 160000 index 0518f864a..000000000 --- a/mongodb +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0518f864afcce2ebb79f1f2edab5de323c811af7 diff --git a/mongodb/.fixtures.yml b/mongodb/.fixtures.yml new file mode 100644 index 000000000..e48e20aae --- /dev/null +++ b/mongodb/.fixtures.yml @@ -0,0 +1,6 @@ +fixtures: + repositories: + "stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib.git" + "apt": "git://github.com/puppetlabs/puppetlabs-apt.git" + symlinks: + "mongodb": "#{source_dir}" diff --git a/mongodb/.gitignore b/mongodb/.gitignore new file mode 100644 index 000000000..9bbbbed4b --- /dev/null +++ b/mongodb/.gitignore @@ -0,0 +1,10 @@ +Gemfile.lock +metadata.json +*.idea +*.swp +*.tmp +tmp/ +pkg/ +spec/fixtures/manifests +.rspec_system/ +.vagrant/ \ No newline at end of file diff --git a/mongodb/.nodeset.yml b/mongodb/.nodeset.yml new file mode 100644 index 000000000..767f9cd2f --- /dev/null +++ b/mongodb/.nodeset.yml @@ -0,0 +1,31 @@ +--- +default_set: 'centos-64-x64' +sets: + 'centos-59-x64': + nodes: + "main.foo.vm": + prefab: 'centos-59-x64' + 'centos-64-x64': + nodes: + "main.foo.vm": + prefab: 'centos-64-x64' + 'fedora-18-x64': + nodes: + "main.foo.vm": + prefab: 'fedora-18-x64' + 'debian-607-x64': + nodes: + "main.foo.vm": + prefab: 'debian-607-x64' + 'debian-70rc1-x64': + nodes: + "main.foo.vm": + prefab: 'debian-70rc1-x64' + 'ubuntu-server-10044-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-10044-x64' + 'ubuntu-server-12042-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-12042-x64' diff --git a/mongodb/.travis.yml b/mongodb/.travis.yml new file mode 100644 index 000000000..f6eff1ce9 --- /dev/null +++ b/mongodb/.travis.yml @@ -0,0 +1,33 @@ +branches: + only: + - master +language: ruby +before_install: + - gem update bundler + - bundle --version + - gem update --system 2.1.11 + - gem --version +bundler_args: --without development +script: "bundle exec rake spec SPEC_OPTS='--format documentation'" +after_success: + - git clone -q git://github.com/puppetlabs/ghpublisher.git .forge-release + - .forge-release/publish +rvm: + - 1.8.7 + - 1.9.3 + - 2.0.0 +env: + matrix: + - PUPPET_GEM_VERSION="~> 2.7.0" + - PUPPET_GEM_VERSION="~> 3.3.0" + global: + - PUBLISHER_LOGIN=puppetlabs + - secure: "iUYpjvk33JffZB9lVRqjuwRWesvcvmTknh908xnf60rUOA0QbGEPXxQY+LsQJEIimVsMA22fV6vp9BcqMEjO7OfK2MvAWsEWU/lG+kisFqhWDRf96sADE7k/RvPWJeB2xe+lWXK7Eh26jgctNfk4NptX1X1MjGmdzEvH7Aq79/w=" +matrix: + exclude: + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 2.7.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 2.7.0" +notifications: + email: false diff --git a/mongodb/CHANGELOG b/mongodb/CHANGELOG new file mode 100644 index 000000000..92a220f48 --- /dev/null +++ b/mongodb/CHANGELOG @@ -0,0 +1,74 @@ +##2014-05-27 - Release 0.8.0 + +This feature features a rewritten mongodb_replset{} provider, includes several +important bugfixes, ruby 1.8 support, and two new features. + +####Features +- Rewritten mongodb_replset{}, featuring puppet resource support, prefetching, +and flushing. +- Add Ruby 1.8 compatibility. +- Adds `syslog`, allowing you to configure mongodb to send all logging to the hosts syslog. +- Add mongodb::replset, a wrapper class for hiera users. +- Improved testing! + +####Bugfixes +- Fixes the package names to work since 10gen renamed them again. +- Fix provider name in the README. +- Disallow `nojournal` and `journal` to be set at the same time. +- Changed - to = for versioned install on Ubuntu. + +####Known Bugs +* No known bugs + +2014-1-29 - Version 0.7.0 + +Summary: + +Added Replica Set Type and Provider + +2014-1-17 - Version 0.6.0 + +Summary: + +Added support for installing MongoDB client on +RHEL family systems. + +2014-01-10 Version 0.5.0 + +Summary: + +Added types for providers for Mongo users and databases. + +2013-12 Version 0.4.0 + +Major refactoring of the MongoDB module. Includes a new 'mongodb::globals' +that consolidates many shared parameters into one location. This is an +API-breaking release in anticipation of a 1.0 release. + +2013-10-31 - Version 0.3.0 + +Summary: + +Adds a number of parameters and fixes some platform +specific bugs in module deployment. + +2013-09-25 - Version 0.2.0 + +Summary: + +This release fixes a duplicate parameter. + +Fixes: +- Fix a duplicated parameter. + +2012-07-13 Puppet Labs - 0.1.0 +* Add support for RHEL/CentOS +* Change default mongodb install location to OS repo + +2012-05-29 Puppet Labs - 0.0.2 +* Fix Modulefile typo. +* Remove repo pin. +* Update spec tests and add travis support. + +2012-05-03 Puppet Labs - 0.0.1 +* Initial Release. diff --git a/mongodb/Gemfile b/mongodb/Gemfile new file mode 100644 index 000000000..d0c14e046 --- /dev/null +++ b/mongodb/Gemfile @@ -0,0 +1,20 @@ +source ENV['GEM_SOURCE'] || 'https://rubygems.org' + +group :test, :development do + gem 'rspec-puppet', :require => false + gem 'rake', :require => false + gem 'puppetlabs_spec_helper', :require => false + gem 'serverspec', :require => false + gem 'puppet-lint', :require => false + gem 'pry', :require => false + gem 'simplecov', :require => false + gem 'beaker', :require => false + gem 'beaker-rspec', :require => false + gem 'vagrant-wrapper', :require => false +end + +if puppetversion = ENV['PUPPET_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end diff --git a/mongodb/LICENSE b/mongodb/LICENSE new file mode 100644 index 000000000..8961ce8a6 --- /dev/null +++ b/mongodb/LICENSE @@ -0,0 +1,15 @@ +Copyright (C) 2012 Puppet Labs Inc + +Puppet Labs can be contacted at: info@puppetlabs.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/mongodb/Modulefile b/mongodb/Modulefile new file mode 100644 index 000000000..e2eb82771 --- /dev/null +++ b/mongodb/Modulefile @@ -0,0 +1,12 @@ +name 'puppetlabs-mongodb' +version '0.8.0' +source 'git@github.com:puppetlabs/puppetlabs-mongodb.git' +author 'puppetlabs' +license 'Apache License Version 2.0' +summary 'mongodb puppet module' +description '10gen mongodb puppet module' +project_page 'https://github.com/puppetlabs/puppetlabs-mongodb' + +## Add dependencies, if any: +dependency 'puppetlabs/apt', '>= 1.0.0' +dependency 'puppetlabs/stdlib', '>= 2.2.0' diff --git a/mongodb/README.md b/mongodb/README.md new file mode 100644 index 000000000..9462cbd49 --- /dev/null +++ b/mongodb/README.md @@ -0,0 +1,515 @@ +# mongodb puppet module + +[![Build Status](https://travis-ci.org/puppetlabs/puppetlabs-mongodb.png?branch=master)](https://travis-ci.org/puppetlabs/puppetlabs-mongodb) + +####Table of Contents + +1. [Overview] (#overview) +2. [Module Description - What does the module do?](#module-description) +3. [Setup - The basics of getting started with mongodb](#setup) +4. [Usage - Configuration options and additional functionality](#usage) +5. [Reference - An under-the-hood peek at what the module is doing and how](#reference) +6. [Limitations - OS compatibility, etc.] (#limitations) +7. [Development - Guide for contributing to the module] (#development) + +## Overview + +Installs MongoDB on RHEL/Ubuntu/Debian from OS repo, or alternatively from +10gen repository [installation documentation](http://www.mongodb.org/display/DOCS/Ubuntu+and+Debian+packages). + +### Deprecation Warning ### + +This release is a major refactoring of the module which means that the API may +have changed in backwards incompatible ways. If your project depends on the old API, +please pin your dependencies to 0.3 version to ensure your environments don't break. + +The current module design is undergoing review for potential 1.0 release. We welcome +any feedback with regard to the APIs and patterns used in this release. + +##Module Description + +The MongoDB module manages mongod server installation and configuration of the +mongod daemon. For the time being it supports only a single MongoDB server +instance, without sharding functionality. + +For the 0.5 release, the MongoDB module now supports database and user types. + +For the 0.6 release, the MongoDB module now supports basic replicaset features +(initiating a replicaset and adding members, but without specific options). + +## Setup + +###What MongoDB affects + +* MongoDB package. +* MongoDB configuration files. +* MongoDB service. +* MongoDB client. +* 10gen/mongodb apt/yum repository. + +###Beginning with MongoDB + +If you just want a server installation with the default options you can run +`include '::mongodb::server'`. If you need to customize configuration +options you need to do the following: + +```puppet +class {'::mongodb::server': + port => 27018, + verbose => true, +} +``` + +For Red Hat family systems, the client can be installed in a similar fashion: + +``` +puppet class {'::mongodb::client':} +``` + +Note that for Debian/Ubuntu family systems the client is installed with the +server. Using the client class will by default install the server. + +Although most distros come with a prepacked MongoDB server we recommend to +use the 10gen/MongoDB software repository, because most of the current OS +packages are outdated and not appropriate for a production environment. +To install MongoDB from 10gen repository: + +```puppet +class {'::mongodb::globals': + manage_package_repo => true, +}-> +class {'::mongodb::server': }-> +class {'::mongodb::client': } +``` + +## Usage + +Most of the interaction for the server is done via `mongodb::server`. For +more options please have a look at [mongodb::server](#class-mongodbserver). +Also in this version we introduced `mongodb::globals`, which is meant more +for future implementation, where you can configure the main settings for +this module in a global way, to be used by other classes and defined resources. +On its own it does nothing. + +### Create MongoDB database + +To install MongoDB server, create database "testdb" and user "user1" with password "pass1". + +```puppet +class {'::mongodb::server': + auth => true, +} + +mongodb::db { 'testdb': + user => 'user1', + password_hash => 'a15fbfca5e3a758be80ceaf42458bcd8', +} +``` +Parameter 'password_hash' is hex encoded md5 hash of "user1:mongo:pass1". +Unsafe plain text password could be used with 'password' parameter instead of 'password_hash'. + +## Reference + +### Classes + +####Public classes +* `mongodb::server`: Installs and configure MongoDB +* `mongodb::client`: Installs the MongoDB client shell (for Red Hat family systems) +* `mongodb::globals`: Configure main settings in a global way + +####Private classes +* `mongodb::repo`: Manage 10gen/MongoDB software repository +* `mongodb::repo::apt`: Manage Debian/Ubuntu apt 10gen/MongoDB repository +* `mongodb::repo::yum`: Manage Redhat/CentOS apt 10gen/MongoDB repository +* `mongodb::server::config`: Configures MongoDB configuration files +* `mongodb::server::install`: Install MongoDB software packages +* `mongodb::server::service`: Manages service +* `mongodb::client::install`: Installs the MongoDB client software package + +####Class: mongodb::globals +*Note:* most server specific defaults should be overridden in the `mongodb::server` +class. This class should only be used if you are using a non-standard OS or +if you are changing elements such as `version` or `manage_package_repo` that +can only be changed here. + +This class allows you to configure the main settings for this module in a +global way, to be used by the other classes and defined resources. On its +own it does nothing. + +#####`server_package_name` +This setting can be used to override the default MongoDB server package +name. If not specified, the module will use whatever package name is the +default for your OS distro. + +#####`service_name` +This setting can be used to override the default MongoDB service name. If not +specified, the module will use whatever service name is the default for your OS distro. + +#####`service_provider` +This setting can be used to override the default MongoDB service provider. If +not specified, the module will use whatever service provider is the default for +your OS distro. + +#####`service_status` +This setting can be used to override the default status check command for +your MongoDB service. If not specified, the module will use whatever service +name is the default for your OS distro. + +#####`user` +This setting can be used to override the default MongoDB user and owner of the +service and related files in the file system. If not specified, the module will +use the default for your OS distro. + +#####`group` +This setting can be used to override the default MongoDB user group to be used +for related files in the file system. If not specified, the module will use +the default for your OS distro. + +#####`bind_ip` +This setting can be used to configure MonogDB process to bind to and listen +for connections from applications on this address. If not specified, the +module will use the default for your OS distro. +*Note:* This value should be passed as an array. + +#####`version` +The version of MonogDB to install/manage. This is a simple way of providing +a specific version such as '2.2' or '2.4' for example. If not specified, +the module will use the default for your OS distro. + +####Class: mongodb::server + +Most of the parameters manipulate the mongod.conf file. + +For more details about configuration parameters consult the +[MongoDB Configuration File Options](http://docs.mongodb.org/manual/reference/configuration-options/). + +#####`ensure` +Used to ensure that the package is installed and the service is running, or that the package is absent/purged and the service is stopped. Valid values are true/false/present/absent/purged. + +#####`config` +Path of the config file. If not specified, the module will use the default +for your OS distro. + +#####`dbpath` +Set this value to designate a directory for the mongod instance to store +it's data. If not specified, the module will use the default for your OS distro. + +#####`pidfilepath` +Specify a file location to hold the PID or process ID of the mongod process. +If not specified, the module will use the default for your OS distro. + +#####`logpath` +Specify the path to a file name for the log file that will hold all diagnostic +logging information. Unless specified, mongod will output all log information +to the standard output. + +#####`bind_ip` +Set this option to configure the mongod or mongos process to bind to and listen +for connections from applications on this address. If not specified, the module +will use the default for your OS distro. Example: bind_ip=['127.0.0.1', '192.168.0.3'] +*Note*: bind_ip accepts an array as a value. + +#####`logappend` +Set to true to add new entries to the end of the logfile rather than overwriting +the content of the log when the process restarts. Default: True + +#####`fork` +Set to true to fork server process at launch time. The default setting depends on +the operating system. + +#####`port` +Specifies a TCP port for the server instance to listen for client connections. +Default: 27017 + +#####`journal` +Set to true to enable operation journaling to ensure write durability and +data consistency. Default: on 64-bit systems true and on 32-bit systems false + +#####`nojournal` +Set nojournal = true to disable durability journaling. By default, mongod +enables journaling in 64-bit versions after v2.0. +Default: on 64-bit systems false and on 32-bit systems true + +*Note*: You must use journal to enable journaling on 32-bit systems. + +#####`smallfiles` +Set to true to modify MongoDB to use a smaller default data file size. +Specifically, smallfiles reduces the initial size for data files and +limits them to 512 megabytes. Default: false + +#####`cpu` +Set to true to force mongod to report every four seconds CPU utilization +and the amount of time that the processor waits for I/O operations to +complete (i.e. I/O wait.) Default: false + +#####`auth` +Set to true to enable database authentication for users connecting from +remote hosts. If no users exist, the localhost interface will continue +to have access to the database until you create the first user. +Default: false + +#####`noauth` +Disable authentication. Currently the default. Exists for future compatibility + and clarity. + +#####`verbose` +Increases the amount of internal reporting returned on standard output or in +the log file generated by `logpath`. Default: false + +#####`verbositylevel` +MongoDB has the following levels of verbosity: v, vv, vvv, vvvv and vvvvv. +Default: None + +#####`objcheck` +Forces the mongod to validate all requests from clients upon receipt to ensure +that clients never insert invalid documents into the database. +Default: on v2.4 default to true and on earlier version to false + +#####`quota` +Set to true to enable a maximum limit for the number of data files each database +can have. The default quota is 8 data files, when quota is true. Default: false + +#####`quotafiles` +Modify limit on the number of data files per database. This option requires the +`quota` setting. Default: 8 + +#####`diaglog` +Creates a very verbose diagnostic log for troubleshooting and recording various +errors. Valid values: 0, 1, 2, 3 and 7. +For more information please refer to [MongoDB Configuration File Options](http://docs.mongodb.org/manual/reference/configuration-options/). + +#####`directoryperdb` +Set to true to modify the storage pattern of the data directory to store each +database’s files in a distinct folder. Default: false + +#####`profile` +Modify this value to changes the level of database profiling, which inserts +information about operation performance into output of mongod or the +log file if specified by `logpath`. + +#####`maxconns` +Specifies a value to set the maximum number of simultaneous connections +that MongoDB will accept. Default: depends on system (i.e. ulimit and file descriptor) +limits. Unless set, MongoDB will not limit its own connections. + +#####`oplog_size` +Specifies a maximum size in megabytes for the replication operation log +(e.g. oplog.) mongod creates an oplog based on the maximum amount of space +available. For 64-bit systems, the oplog is typically 5% of available disk space. + +#####`nohints` +Ignore query hints. Default: None + +#####`nohttpinterface` +Set to true to disable the HTTP interface. This command will override the rest +and disable the HTTP interface if you specify both. Default: false + +#####`noscripting` +Set noscripting = true to disable the scripting engine. Default: false + +#####`notablescan` +Set notablescan = true to forbid operations that require a table scan. Default: false + +#####`noprealloc` +Set noprealloc = true to disable the preallocation of data files. This will shorten +the start up time in some cases, but can cause significant performance penalties +during normal operations. Default: false + +#####`nssize` +Use this setting to control the default size for all newly created namespace +files (i.e .ns). Default: 16 + +#####`mms_token` +MMS token for mms monitoring. Default: None + +#####`mms_name` +MMS identifier for mms monitoring. Default: None + +#####`mms_interval` +MMS interval for mms monitoring. Default: None + +#####`replset` +Use this setting to configure replication with replica sets. Specify a replica +set name as an argument to this set. All hosts must have the same set name. + +#####`rest` +Set to true to enable a simple REST interface. Default: false + +#####`slowms` +Sets the threshold for mongod to consider a query “slow” for the database profiler. +Default: 100 ms + +#####`keyfile` +Specify the path to a key file to store authentication information. This option +is only useful for the connection between replica set members. Default: None + +#####`master` +Set to true to configure the current instance to act as master instance in a +replication configuration. Default: False *Note*: deprecated – use replica sets + +#####`set_parameter` +Specify extra configuration file parameters (i.e. +textSearchEnabled=true). Default: None + +#####`syslog` +Sends all logging output to the host’s syslog system rather than to standard +output or a log file. Default: None +*Important*: You cannot use syslog with logpath. + +#####`slave` +Set to true to configure the current instance to act as slave instance in a +replication configuration. Default: false +*Note*: deprecated – use replica sets + +#####`only` +Used with the slave option, only specifies only a single database to +replicate. Default: <> +*Note*: deprecated – use replica sets + +#####`source` +Used with the slave setting to specify the master instance from which +this slave instance will replicate. Default: <> +*Note*: deprecated – use replica sets + +### Definitions + +#### Definition: mongodb:db + +Creates database with user. Resource title used as database name. + +#####`user` +Name of the user for database + +#####`password_hash` +Hex encoded md5 hash of "$username:mongo:$password". +For more information please refer to [MongoDB Authentication Process](http://docs.mongodb.org/meta-driver/latest/legacy/implement-authentication-in-driver/#authentication-process). + +#####`password` +Plain-text user password (will be hashed) + +#####`roles` +Array with user roles. Default: ['dbAdmin'] + +### Providers + +#### Provider: mongodb_database +'mongodb_database' can be used to create and manage databases within MongoDB. + +```puppet +mongodb_database { testdb: + ensure => present, + tries => 10, + require => Class['mongodb::server'], +} +``` +#####`tries` +The maximum amount of two second tries to wait MongoDB startup. Default: 10 + + +#### Provider: mongodb_user +'mongodb_user' can be used to create and manage users within MongoDB database. + +```puppet +mongodb_user { testuser: + ensure => present, + password_hash => mongodb_password('testuser', 'p@ssw0rd'), + database => testdb, + roles => ['readWrite', 'dbAdmin'], + tries => 10, + require => Class['mongodb::server'], +} +``` +#####`password_hash` +Hex encoded md5 hash of "$username:mongo:$password". + +#####`database` +Name of database. It will be created, if not exists. + +#####`roles` +Array with user roles. Default: ['dbAdmin'] + +#####`tries` +The maximum amount of two second tries to wait MongoDB startup. Default: 10 + +#### Provider: mongodb_replset +'mongodb_replset' can be used to create and manage MongoDB replicasets. + +```puppet +mongodb_replset { rsmain: + ensure => present, + members => ['host1:27017', 'host2:27017', 'host3:27017'] +} +``` + +Ideally the ```mongodb_replset``` resource will be declared on the initial +desired primary node (arbitrarily the first of the list) and this node will be +processed once the secondary nodes are up. This will ensure all the nodes are +in the first configuration of the replicaset, else it will require running +puppet again to add them. + +#####`members` +Array of 'host:port' of the replicaset members. + +It currently only adds members without options. + +## Limitations + +This module has been tested on: + +* Debian 7.* (Wheezy) +* Debian 6.* (squeeze) +* Ubuntu 12.04.2 (precise) +* Ubuntu 10.04.4 LTS (lucid) +* RHEL 5/6 +* CentOS 5/6 + +For a full list of tested operating systems please have a look at the [.nodeset.xml](https://github.com/puppetlabs/puppetlabs-mongodb/blob/master/.nodeset.yml) definition. + +This module should support `service_ensure` separate from the `ensure` value on `Class[mongodb::server]` but it does not yet. + +## Development + +Puppet Labs modules on the Puppet Forge are open projects, and community +contributions are essential for keeping them great. We can’t access the +huge number of platforms and myriad of hardware, software, and deployment +configurations that Puppet is intended to serve. + +We want to keep it as easy as possible to contribute changes so that our +modules work in your environment. There are a few guidelines that we need +contributors to follow so that we can have a chance of keeping on top of things. + +You can read the complete module contribution guide [on the Puppet Labs wiki.](http://projects.puppetlabs.com/projects/module-site/wiki/Module_contributing) + +### Testing + +There are two types of tests distributed with this module. Unit tests with +rspec-puppet and system tests using rspec-system. + + +unit tests should be run under Bundler with the gem versions as specified +in the Gemfile. To install the necessary gems: + + bundle install --path=vendor + +Test setup and teardown is handled with rake tasks, so the +supported way of running tests is with + + bundle exec rake spec + + +For system test you will also need to install vagrant > 1.3.x and virtualbox > 4.2.10. +To run the system tests + + bundle exec rake spec:system + +To run the tests on different operating systems, see the sets available in [.nodeset.xml](https://github.com/puppetlabs/puppetlabs-mongodb/blob/master/.nodeset.yml) +and run the specific set with the following syntax: + + RSPEC_SET=ubuntu-server-12042-x64 bundle exec rake spec:system + +### Authors + +We would like to thank everyone who has contributed issues and pull requests to this module. +A complete list of contributors can be found on the +[GitHub Contributor Graph](https://github.com/puppetlabs/puppetlabs-mongodb/graphs/contributors) +for the [puppetlabs-mongodb module](https://github.com/puppetlabs/puppetlabs-mongodb). diff --git a/mongodb/Rakefile b/mongodb/Rakefile new file mode 100644 index 000000000..cd3d37995 --- /dev/null +++ b/mongodb/Rakefile @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/rake_tasks' diff --git a/mongodb/lib/puppet/parser/functions/mongodb_password.rb b/mongodb/lib/puppet/parser/functions/mongodb_password.rb new file mode 100644 index 000000000..e61bcb9da --- /dev/null +++ b/mongodb/lib/puppet/parser/functions/mongodb_password.rb @@ -0,0 +1,14 @@ +require 'digest/md5' + +module Puppet::Parser::Functions + newfunction(:mongodb_password, :type => :rvalue, :doc => <<-EOS + Returns the mongodb password hash from the clear text password. + EOS + ) do |args| + + raise(Puppet::ParseError, 'mongodb_password(): Wrong number of arguments ' + + "given (#{args.size} for 2)") if args.size != 2 + + Digest::MD5.hexdigest("#{args[0]}:mongo:#{args[1]}") + end +end diff --git a/mongodb/lib/puppet/provider/mongodb_conn_validator/tcp_port.rb b/mongodb/lib/puppet/provider/mongodb_conn_validator/tcp_port.rb new file mode 100644 index 000000000..f913696b7 --- /dev/null +++ b/mongodb/lib/puppet/provider/mongodb_conn_validator/tcp_port.rb @@ -0,0 +1,53 @@ +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__),"..","..","..")) +require 'puppet/util/mongodb_validator' + +# This file contains a provider for the resource type `mongodb_conn_validator`, +# which validates the mongodb connection by attempting an https connection. + +Puppet::Type.type(:mongodb_conn_validator).provide(:tcp_port) do + desc "A provider for the resource type `mongodb_conn_validator`, + which validates the mongodb connection by attempting an https + connection to the mongodb server. Uses the puppet SSL certificate + setup from the local puppet environment to authenticate." + + def exists? + start_time = Time.now + timeout = resource[:timeout] + + success = validator.attempt_connection + + while success == false && ((Time.now - start_time) < timeout) + # It can take several seconds for the mongodb server to start up; + # especially on the first install. Therefore, our first connection attempt + # may fail. Here we have somewhat arbitrarily chosen to retry every 4 + # seconds until the configurable timeout has expired. + Puppet.debug("Failed to connect to mongodb; sleeping 4 seconds before retry") + sleep 4 + success = validator.attempt_connection + end + + if success + Puppet.debug("Connected to mongodb in #{Time.now - start_time} seconds.") + else + Puppet.notice("Failed to connect to mongodb within timeout window of #{timeout} seconds; giving up.") + end + + success + end + + def create + # If `#create` is called, that means that `#exists?` returned false, which + # means that the connection could not be established... so we need to + # cause a failure here. + raise Puppet::Error, "Unable to connect to mongodb server! (#{@validator.mongodb_server}:#{@validator.mongodb_port})" + end + + private + + # @api private + def validator + @validator ||= Puppet::Util::MongodbValidator.new(resource[:server], resource[:port]) + end + +end + diff --git a/mongodb/lib/puppet/provider/mongodb_database/mongodb.rb b/mongodb/lib/puppet/provider/mongodb_database/mongodb.rb new file mode 100644 index 000000000..2de8420cc --- /dev/null +++ b/mongodb/lib/puppet/provider/mongodb_database/mongodb.rb @@ -0,0 +1,36 @@ +Puppet::Type.type(:mongodb_database).provide(:mongodb) do + + desc "Manages MongoDB database." + + defaultfor :kernel => 'Linux' + + commands :mongo => 'mongo' + + def block_until_mongodb(tries = 10) + begin + mongo('--quiet', '--eval', 'db.getMongo()') + rescue => e + debug('MongoDB server not ready, retrying') + sleep 2 + if (tries -= 1) > 0 + retry + else + raise e + end + end + end + + def create + mongo(@resource[:name], '--quiet', '--eval', "db.dummyData.insert({\"created_by_puppet\": 1})") + end + + def destroy + mongo(@resource[:name], '--quiet', '--eval', 'db.dropDatabase()') + end + + def exists? + block_until_mongodb(@resource[:tries]) + mongo("--quiet", "--eval", 'db.getMongo().getDBNames()').chomp.split(",").include?(@resource[:name]) + end + +end diff --git a/mongodb/lib/puppet/provider/mongodb_replset/mongo.rb b/mongodb/lib/puppet/provider/mongodb_replset/mongo.rb new file mode 100644 index 000000000..d77afe303 --- /dev/null +++ b/mongodb/lib/puppet/provider/mongodb_replset/mongo.rb @@ -0,0 +1,232 @@ +# +# Author: François Charlier +# + +Puppet::Type.type(:mongodb_replset).provide(:mongo) do + + desc "Manage hosts members for a replicaset." + + confine :true => + begin + require 'json' + true + rescue LoadError + false + end + + commands :mongo => 'mongo' + + mk_resource_methods + + def initialize(resource={}) + super(resource) + @property_flush = {} + end + + def members=(hosts) + @property_flush[:members] = hosts + end + + def self.instances + instance = get_replset_properties + if instance + # There can only be one replset per node + [new(instance)] + else + [] + end + end + + def self.prefetch(resources) + instances.each do |prov| + if resource = resources[prov.name] + resource.provider = prov + end + end + end + + def exists? + @property_hash[:ensure] == :present + end + + def create + @property_flush[:ensure] = :present + @property_flush[:members] = resource.should(:members) + end + + def destroy + @property_flush[:ensure] = :absent + end + + def flush + set_members + @property_hash = self.class.get_replset_properties + end + + private + + def db_ismaster(host) + mongo_command("db.isMaster()", host) + end + + def rs_initiate(conf, master) + return mongo_command("rs.initiate(#{conf})", master) + end + + def rs_status(host) + mongo_command("rs.status()", host) + end + + def rs_add(host, master) + mongo_command("rs.add(\"#{host}\")", master) + end + + def rs_remove(host, master) + mongo_command("rs.remove(\"#{host}\")", master) + end + + def master_host(hosts) + hosts.each do |host| + status = db_ismaster(host) + if status.has_key?('primary') + return status['primary'] + end + end + false + end + + def self.get_replset_properties + output = mongo_command('rs.conf()') + if output['members'] + members = output['members'].collect do |val| + val['host'] + end + props = { + :name => output['_id'], + :ensure => :present, + :members => members, + :provider => :mongo, + } + else + props = nil + end + Puppet.debug("MongoDB replset properties: #{props.inspect}") + props + end + + def alive_members(hosts) + hosts.select do |host| + begin + Puppet.debug "Checking replicaset member #{host} ..." + status = rs_status(host) + if status.has_key?('errmsg') and status['errmsg'] == 'not running with --replSet' + raise Puppet::Error, "Can't configure replicaset #{self.name}, host #{host} is not supposed to be part of a replicaset." + end + if status.has_key?('set') + if status['set'] != self.name + raise Puppet::Error, "Can't configure replicaset #{self.name}, host #{host} is already part of another replicaset." + end + + # This node is alive and supposed to be a member of our set + Puppet.debug "Host #{self.name} is available for replset #{status['set']}" + true + elsif status.has_key?('info') + Puppet.debug "Host #{self.name} is alive but unconfigured: #{status['info']}" + true + end + rescue Puppet::ExecutionFailure + Puppet.warning "Can't connect to replicaset member #{host}." + + false + end + end + end + + def set_members + if @property_flush[:ensure] == :absent + # TODO: I don't know how to remove a node from a replset; unimplemented + #Puppet.debug "Removing all members from replset #{self.name}" + #@property_hash[:members].collect do |member| + # rs_remove(member, master_host(@property_hash[:members])) + #end + return + end + + if ! @property_flush[:members].empty? + # Find the alive members so we don't try to add dead members to the replset + alive_hosts = alive_members(@property_flush[:members]) + dead_hosts = @property_flush[:members] - alive_hosts + raise Puppet::Error, "Can't connect to any member of replicaset #{self.name}." if alive_hosts.empty? + Puppet.debug "Alive members: #{alive_hosts.inspect}" + Puppet.debug "Dead members: #{dead_hosts.inspect}" unless dead_hosts.empty? + else + alive_hosts = [] + end + + if @property_flush[:ensure] == :present and @property_hash[:ensure] != :present + Puppet.debug "Initializing the replset #{self.name}" + + # Create a replset configuration + hostconf = alive_hosts.each_with_index.map do |host,id| + "{ _id: #{id}, host: \"#{host}\" }" + end.join(',') + conf = "{ _id: \"#{self.name}\", members: [ #{hostconf} ] }" + + # Set replset members with the first host as the master + output = rs_initiate(conf, alive_hosts[0]) + if output['ok'] == 0 + raise Puppet::Error, "rs.initiate() failed for replicaset #{self.name}: #{output['errmsg']}" + end + else + # Add members to an existing replset + if master = master_host(alive_hosts) + current_hosts = db_ismaster(master)['hosts'] + newhosts = alive_hosts - current_hosts + newhosts.each do |host| + output = rs_add(host, master) + if output['ok'] == 0 + raise Puppet::Error, "rs.add() failed to add host to replicaset #{self.name}: #{output['errmsg']}" + end + end + else + raise Puppet::Error, "Can't find master host for replicaset #{self.name}." + end + end + end + + def mongo_command(command, host, retries=4) + self.class.mongo_command(command,host,retries) + end + + def self.mongo_command(command, host=nil, retries=4) + # Allow waiting for mongod to become ready + # Wait for 2 seconds initially and double the delay at each retry + wait = 2 + begin + args = Array.new + args << '--quiet' + args << ['--host',host] if host + args << ['--eval',"printjson(#{command})"] + output = mongo(args.flatten) + rescue Puppet::ExecutionFailure => e + if e =~ /Error: couldn't connect to server/ and wait <= 2**max_wait + info("Waiting #{wait} seconds for mongod to become available") + sleep wait + wait *= 2 + retry + else + raise + end + end + + # Dirty hack to remove JavaScript objects + output.gsub!(/ISODate\((.+?)\)/, '\1 ') + output.gsub!(/Timestamp\((.+?)\)/, '[\1]') + + #Hack to avoid non-json empty sets + output = "{}" if output == "null\n" + + JSON.parse(output) + end + +end diff --git a/mongodb/lib/puppet/provider/mongodb_user/mongodb.rb b/mongodb/lib/puppet/provider/mongodb_user/mongodb.rb new file mode 100644 index 000000000..10e0bf7f0 --- /dev/null +++ b/mongodb/lib/puppet/provider/mongodb_user/mongodb.rb @@ -0,0 +1,48 @@ +Puppet::Type.type(:mongodb_user).provide(:mongodb) do + + desc "Manage users for a MongoDB database." + + defaultfor :kernel => 'Linux' + + commands :mongo => 'mongo' + + def block_until_mongodb(tries = 10) + begin + mongo('--quiet', '--eval', 'db.getMongo()') + rescue + debug('MongoDB server not ready, retrying') + sleep 2 + retry unless (tries -= 1) <= 0 + end + end + + def create + mongo(@resource[:database], '--eval', "db.system.users.insert({user:\"#{@resource[:name]}\", pwd:\"#{@resource[:password_hash]}\", roles: #{@resource[:roles].inspect}})") + end + + def destroy + mongo(@resource[:database], '--quiet', '--eval', "db.removeUser(\"#{@resource[:name]}\")") + end + + def exists? + block_until_mongodb(@resource[:tries]) + mongo(@resource[:database], '--quiet', '--eval', "db.system.users.find({user:\"#{@resource[:name]}\"}).count()").strip.eql?('1') + end + + def password_hash + mongo(@resource[:database], '--quiet', '--eval', "db.system.users.findOne({user:\"#{@resource[:name]}\"})[\"pwd\"]").strip + end + + def password_hash=(value) + mongo(@resource[:database], '--quiet', '--eval', "db.system.users.update({user:\"#{@resource[:name]}\"}, { $set: {pwd:\"#{value}\"}})") + end + + def roles + mongo(@resource[:database], '--quiet', '--eval', "db.system.users.findOne({user:\"#{@resource[:name]}\"})[\"roles\"]").strip.split(",").sort + end + + def roles=(value) + mongo(@resource[:database], '--quiet', '--eval', "db.system.users.update({user:\"#{@resource[:name]}\"}, { $set: {roles: #{@resource[:roles].inspect}}})") + end + +end diff --git a/mongodb/lib/puppet/type/mongodb_conn_validator.rb b/mongodb/lib/puppet/type/mongodb_conn_validator.rb new file mode 100644 index 000000000..c67043eae --- /dev/null +++ b/mongodb/lib/puppet/type/mongodb_conn_validator.rb @@ -0,0 +1,43 @@ +Puppet::Type.newtype(:mongodb_conn_validator) do + + @doc = "Verify that a connection can be successfully established between a node + and the mongodb server. Its primary use is as a precondition to + prevent configuration changes from being applied if the mongodb + server cannot be reached, but it could potentially be used for other + purposes such as monitoring." + + ensurable do + defaultvalues + defaultto :present + end + + newparam(:name, :namevar => true) do + desc 'An arbitrary name used as the identity of the resource.' + end + + newparam(:server) do + desc 'An array containing DNS names or IP addresses of the server where mongodb should be running.' + munge do |value| + Array(value).first + end + end + + newparam(:port) do + desc 'The port that the mongodb server should be listening on.' + end + + newparam(:timeout) do + desc 'The max number of seconds that the validator should wait before giving up and deciding that puppetdb is not running; defaults to 60 seconds.' + defaultto 60 + + validate do |value| + # This will raise an error if the string is not convertible to an integer + Integer(value) + end + + munge do |value| + Integer(value) + end + end + +end diff --git a/mongodb/lib/puppet/type/mongodb_database.rb b/mongodb/lib/puppet/type/mongodb_database.rb new file mode 100644 index 000000000..8be5f2e0a --- /dev/null +++ b/mongodb/lib/puppet/type/mongodb_database.rb @@ -0,0 +1,27 @@ +Puppet::Type.newtype(:mongodb_database) do + @doc = "Manage MongoDB databases." + + ensurable + + newparam(:name, :namevar=>true) do + desc "The name of the database." + newvalues(/^\w+$/) + end + + newparam(:tries) do + desc "The maximum amount of two second tries to wait MongoDB startup." + defaultto 10 + newvalues(/^\d+$/) + munge do |value| + Integer(value) + end + end + + autorequire(:package) do + 'mongodb_client' + end + + autorequire(:service) do + 'mongodb' + end +end diff --git a/mongodb/lib/puppet/type/mongodb_replset.rb b/mongodb/lib/puppet/type/mongodb_replset.rb new file mode 100644 index 000000000..f186657c0 --- /dev/null +++ b/mongodb/lib/puppet/type/mongodb_replset.rb @@ -0,0 +1,35 @@ +# +# Author: François Charlier +# + +Puppet::Type.newtype(:mongodb_replset) do + @doc = "Manage a MongoDB replicaSet" + + ensurable do + defaultto :present + + newvalue(:present) do + provider.create + end + end + + newparam(:name) do + desc "The name of the replicaSet" + end + + newproperty(:members, :array_matching => :all) do + desc "The replicaSet members" + + def insync?(is) + is.sort == should.sort + end + end + + autorequire(:package) do + 'mongodb_client' + end + + autorequire(:service) do + 'mongodb' + end +end diff --git a/mongodb/lib/puppet/type/mongodb_user.rb b/mongodb/lib/puppet/type/mongodb_user.rb new file mode 100644 index 000000000..9cd7b1038 --- /dev/null +++ b/mongodb/lib/puppet/type/mongodb_user.rb @@ -0,0 +1,63 @@ +Puppet::Type.newtype(:mongodb_user) do + @doc = 'Manage a MongoDB user. This includes management of users password as well as privileges.' + + ensurable + + def initialize(*args) + super + # Sort roles array before comparison. + self[:roles] = Array(self[:roles]).sort! + end + + newparam(:name, :namevar=>true) do + desc "The name of the user." + end + + newparam(:database) do + desc "The user's target database." + defaultto do + fail("Parameter 'database' must be set") + end + newvalues(/^\w+$/) + end + + newparam(:tries) do + desc "The maximum amount of two second tries to wait MongoDB startup." + defaultto 10 + newvalues(/^\d+$/) + munge do |value| + Integer(value) + end + end + + newproperty(:roles, :array_matching => :all) do + desc "The user's roles." + defaultto ['dbAdmin'] + newvalue(/^\w+$/) + + # Pretty output for arrays. + def should_to_s(value) + value.inspect + end + + def is_to_s(value) + value.inspect + end + end + + newproperty(:password_hash) do + desc "The password hash of the user. Use mongodb_password() for creating hash." + defaultto do + fail("Property 'password_hash' must be set. Use mongodb_password() for creating hash.") + end + newvalue(/^\w+$/) + end + + autorequire(:package) do + 'mongodb_client' + end + + autorequire(:service) do + 'mongodb' + end +end diff --git a/mongodb/lib/puppet/util/mongodb_validator.rb b/mongodb/lib/puppet/util/mongodb_validator.rb new file mode 100644 index 000000000..86ae740ad --- /dev/null +++ b/mongodb/lib/puppet/util/mongodb_validator.rb @@ -0,0 +1,37 @@ +require 'socket' +require 'timeout' + + +module Puppet + module Util + class MongodbValidator + attr_reader :mongodb_server + attr_reader :mongodb_port + + def initialize(mongodb_server, mongodb_port) + @mongodb_server = mongodb_server + @mongodb_port = mongodb_port + end + + # Utility method; attempts to make an https connection to the mongodb server. + # This is abstracted out into a method so that it can be called multiple times + # for retry attempts. + # + # @return true if the connection is successful, false otherwise. + def attempt_connection + Timeout::timeout(Puppet[:configtimeout]) do + begin + TCPSocket.new(@mongodb_server, @mongodb_port).close + true + rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e + Puppet.debug "Unable to connect to mongodb server (#{@mongodb_server}:#{@mongodb_port}): #{e.message}" + false + end + end + rescue Timeout::Error + false + end + end + end +end + diff --git a/mongodb/manifests/client.pp b/mongodb/manifests/client.pp new file mode 100644 index 000000000..bf557d690 --- /dev/null +++ b/mongodb/manifests/client.pp @@ -0,0 +1,16 @@ +# Class for installing a MongoDB client shell (CLI). +# +# == Parameters +# +# [ensure] Desired ensure state of the package. Optional. +# Defaults to 'true' +# +# [package_name] Name of the package to install the client from. Default +# is repository dependent. +# +class mongodb::client ( + $ensure = $mongodb::params::ensure_client, + $package_name = $mongodb::params::client_package_name, +) inherits mongodb::params { + class { 'mongodb::client::install': } +} diff --git a/mongodb/manifests/client/install.pp b/mongodb/manifests/client/install.pp new file mode 100644 index 000000000..51ed31474 --- /dev/null +++ b/mongodb/manifests/client/install.pp @@ -0,0 +1,28 @@ +# PRIVATE CLASS: do not call directly +class mongodb::client::install { + $package_ensure = $mongodb::client::ensure + $package_name = $mongodb::client::package_name + + case $package_ensure { + true: { + $my_package_ensure = 'present' + } + false: { + $my_package_ensure = 'purged' + } + 'absent': { + $my_package_ensure = 'purged' + } + default: { + $my_package_ensure = $package_ensure + } + } + + if $package_name { + package { 'mongodb_client': + ensure => $my_package_ensure, + name => $package_name, + tag => 'mongodb', + } + } +} diff --git a/mongodb/manifests/db.pp b/mongodb/manifests/db.pp new file mode 100644 index 000000000..708f5d75e --- /dev/null +++ b/mongodb/manifests/db.pp @@ -0,0 +1,43 @@ +# == Class: mongodb::db +# +# Class for creating mongodb databases and users. +# +# == Parameters +# +# user - Database username. +# password_hash - Hashed password. Hex encoded md5 hash of "$username:mongo:$password". +# password - Plain text user password. This is UNSAFE, use 'password_hash' unstead. +# roles (default: ['dbAdmin']) - array with user roles. +# tries (default: 10) - The maximum amount of two second tries to wait MongoDB startup. +# +define mongodb::db ( + $user, + $password_hash = false, + $password = false, + $roles = ['dbAdmin'], + $tries = 10, +) { + + mongodb_database { $name: + ensure => present, + tries => $tries, + require => Class['mongodb::server'], + } + + if $password_hash { + $hash = $password_hash + } elsif $password { + $hash = mongodb_password($user, $password) + } else { + fail("Parameter 'password_hash' or 'password' should be provided to mongodb::db.") + } + + mongodb_user { $user: + ensure => present, + password_hash => $hash, + database => $name, + roles => $roles, + require => Mongodb_database[$name], + } + +} diff --git a/mongodb/manifests/globals.pp b/mongodb/manifests/globals.pp new file mode 100644 index 000000000..0eebcd59b --- /dev/null +++ b/mongodb/manifests/globals.pp @@ -0,0 +1,29 @@ +# Class for setting cross-class global overrides. See README.md for more +# details. + +class mongodb::globals ( + $server_package_name = undef, + $client_package_name = undef, + + $service_enable = undef, + $service_ensure = undef, + $service_name = undef, + $service_provider = undef, + $service_status = undef, + + $user = undef, + $group = undef, + $bind_ip = undef, + + $version = undef, + + $manage_package_repo = undef, +) { + + # Setup of the repo only makes sense globally, so we are doing it here. + if($manage_package_repo) { + class { '::mongodb::repo': + ensure => present, + } + } +} diff --git a/mongodb/manifests/init.pp b/mongodb/manifests/init.pp new file mode 100644 index 000000000..d489731b0 --- /dev/null +++ b/mongodb/manifests/init.pp @@ -0,0 +1,136 @@ +# == Class: mongodb +# +# Direct use of this class is deprecated. Please use mongodb::server +# +# Manage mongodb installations on RHEL, CentOS, Debian and Ubuntu - either +# installing from the 10Gen repo or from EPEL in the case of EL systems. +# +# === Parameters +# +# enable_10gen (default: false) - Whether or not to set up 10gen software repositories +# init (auto discovered) - override init (sysv or upstart) for Debian derivatives +# location - override apt location configuration for Debian derivatives +# packagename (auto discovered) - override the package name +# servicename (auto discovered) - override the service name +# service-enable (default: true) - Enable the service and ensure it is running +# +# === Examples +# +# To install with defaults from the distribution packages on any system: +# include mongodb +# +# To install from 10gen on a EL server +# class { 'mongodb': +# enable_10gen => true, +# } +# +# === Authors +# +# Craig Dunn +# +# === Copyright +# +# Copyright 2013 PuppetLabs +# + +class mongodb ( + # Deprecated parameters + $enable_10gen = undef, + + $init = $mongodb::params::service_provider, + $location = '', + $packagename = undef, + $version = undef, + $servicename = $mongodb::params::service_name, + $service_enable = true, #deprecated + $logpath = $mongodb::params::logpath, + $logappend = true, + $fork = $mongodb::params::fork, + $port = 27017, + $dbpath = $mongodb::params::dbpath, + $journal = undef, + $nojournal = undef, + $smallfiles = undef, + $cpu = undef, + $noauth = undef, + $auth = undef, + $verbose = undef, + $objcheck = undef, + $quota = undef, + $oplog = undef, #deprecated it's on if replica set + $oplog_size = undef, + $nohints = undef, + $nohttpinterface = undef, + $noscripting = undef, + $notablescan = undef, + $noprealloc = undef, + $nssize = undef, + $mms_token = undef, + $mms_name = undef, + $mms_interval = undef, + $slave = undef, + $only = undef, + $master = undef, + $source = undef, + $replset = undef, + $rest = undef, + $slowms = undef, + $keyfile = undef, + $bind_ip = undef, + $pidfilepath = undef +) inherits mongodb::params { + + if $enable_10gen { + fail("Parameter enable_10gen is no longer supported. Please use class { 'mongodb::globals': manage_package_repo => true }") + } + + if $version { + fail("Parameter version is no longer supported. Please use class { 'mongodb::globals': version => VERSION }") + } + + if $oplog { + fail('Parameter is no longer supported. On replica set Oplog is enabled by default.') + } + + notify { 'An attempt has been made below to automatically apply your custom + settings to mongodb::server. Please verify this works in a safe test + environment.': } + + class { 'mongodb::server': + package_name => $packagename, + logpath => $logpath, + logappend => $logappend, + fork => $fork, + port => $port, + dbpath => $dbpath, + journal => $journal, + nojournal => $nojournal, + smallfiles => $smallfiles, + cpu => $cpu, + noauth => $noauth, + verbose => $verbose, + objcheck => $objcheck, + quota => $quota, + oplog_size => $oplog_size, + nohints => $nohints, + nohttpinterface => $nohttpinterface, + noscripting => $noscripting, + notablescan => $notablescan, + noprealloc => $noprealloc, + nssize => $nssize, + mms_token => $mms_token, + mms_name => $mms_name, + mms_interval => $mms_interval, + slave => $slave, + only => $only, + master => $master, + source => $source, + replset => $replset, + rest => $rest, + slowms => $slowms, + keyfile => $keyfile, + bind_ip => $bind_ip, + pidfilepath => $pidfilepath, + } + +} diff --git a/mongodb/manifests/params.pp b/mongodb/manifests/params.pp new file mode 100644 index 000000000..50abeffbc --- /dev/null +++ b/mongodb/manifests/params.pp @@ -0,0 +1,98 @@ +# PRIVATE CLASS: do not use directly +class mongodb::params inherits mongodb::globals { + $ensure = true + $service_enable = pick($service_enable, true) + $service_ensure = pick($service_ensure, 'running') + $service_status = $service_status + $ensure_client = true + + # Amazon Linux's OS Family is 'Linux', operating system 'Amazon'. + case $::osfamily { + 'RedHat', 'Linux': { + + if $mongodb::globals::manage_package_repo { + $user = pick($user, 'mongod') + $group = pick($group, 'mongod') + if $::mongodb::globals::version { + $server_package_name = pick("$::mongodb::globals::server_package_name-${::mongodb::globals::version}", "mongodb-org-server-${::mongodb::globals::version}") + $client_package_name = pick("$::mongodb::globals::client_package_name-${::mongodb::globals::version}", "mongodb-org-shell-${::mongodb::globals::version}") + } else { + $server_package_name = pick($::mongodb::globals::server_package_name, 'mongodb-org-server') + $client_package_name = pick($::mongodb::globals::client_package_name, 'mongodb-org-shell') + } + $service_name = pick($::mongodb::globals::service_name, 'mongod') + $config = '/etc/mongod.conf' + $dbpath = '/var/lib/mongodb' + $logpath = '/var/log/mongodb/mongod.log' + $pidfilepath = '/var/run/mongodb/mongod.pid' + $bind_ip = pick($bind_ip, ['127.0.0.1']) + $fork = true + } else { + # RedHat/CentOS doesn't come with a prepacked mongodb + # so we assume that you are using EPEL repository. + $user = pick($user, 'mongodb') + $group = pick($group, 'mongodb') + $server_package_name = pick($server_package_name, 'mongodb-server') + $client_package_name = pick($client_package_name, 'mongodb') + + $service_name = pick($service_name, 'mongod') + $config = '/etc/mongodb.conf' + $dbpath = '/var/lib/mongodb' + $logpath = '/var/log/mongodb/mongodb.log' + $bind_ip = pick($bind_ip, ['127.0.0.1']) + $pidfilepath = '/var/run/mongodb/mongodb.pid' + $fork = true + $journal = true + } + } + 'Debian': { + if $mongodb::globals::manage_package_repo { + $user = pick($user, 'mongodb') + $group = pick($group, 'mongodb') + if $::mongodb::globals::version { + $server_package_name = pick("${::mongodb::globals::server_package_name}=${::mongodb::globals::version}", "mongodb-org-server-${::mongodb::globals::version}") + $client_package_name = pick("${::mongodb::globals::client_package_name}=${::mongodb::globals::version}", "mongodb-org-shell-${::mongodb::globals::version}") + } + else { + $server_package_name = pick($::mongodb::globals::server_package_name, 'mongodb-org-server') + $client_package_name = pick($::mongodb::globals::client_package_name, 'mongodb-org-shell') + } + $service_name = pick($::mongodb::globals::service_name, 'mongod') + $config = '/etc/mongod.conf' + $dbpath = '/var/lib/mongodb' + $logpath = '/var/log/mongodb/mongodb.log' + $bind_ip = ['127.0.0.1'] + } else { + # although we are living in a free world, + # I would not recommend to use the prepacked + # mongodb server on Ubuntu 12.04 or Debian 6/7, + # because its really outdated + $user = pick($user, 'mongodb') + $group = pick($group, 'mongodb') + $server_package_name = pick($server_package_name, 'mongodb-server') + $client_package_name = $client_package_name + $service_name = pick($service_name, 'mongodb') + $config = '/etc/mongodb.conf' + $dbpath = '/var/lib/mongodb' + $logpath = '/var/log/mongodb/mongodb.log' + $bind_ip = pick($bind_ip, ['127.0.0.1']) + $pidfilepath = undef + } + # avoid using fork because of the init scripts design + $fork = undef + } + default: { + fail("Osfamily ${::osfamily} and ${::operatingsystem} is not supported") + } + } + + case $::operatingsystem { + 'Ubuntu': { + $service_provider = pick($service_provider, 'upstart') + } + default: { + $service_provider = undef + } + } + +} diff --git a/mongodb/manifests/replset.pp b/mongodb/manifests/replset.pp new file mode 100644 index 000000000..ce4a02555 --- /dev/null +++ b/mongodb/manifests/replset.pp @@ -0,0 +1,10 @@ +# Wrapper class useful for hiera based deployments + +class mongodb::replset( + $sets = undef +) { + + if $sets { + create_resources(mongodb_replset, $sets) + } +} diff --git a/mongodb/manifests/repo.pp b/mongodb/manifests/repo.pp new file mode 100644 index 000000000..ecd6de1b7 --- /dev/null +++ b/mongodb/manifests/repo.pp @@ -0,0 +1,31 @@ +# PRIVATE CLASS: do not use directly +class mongodb::repo ( + $ensure = $mongodb::params::ensure, +) inherits mongodb::params { + case $::osfamily { + 'RedHat', 'Linux': { + $location = $::architecture ? { + 'x86_64' => 'http://downloads-distro.mongodb.org/repo/redhat/os/x86_64/', + 'i686' => 'http://downloads-distro.mongodb.org/repo/redhat/os/i686/', + 'i386' => 'http://downloads-distro.mongodb.org/repo/redhat/os/i686/', + default => undef + } + class { 'mongodb::repo::yum': } + } + + 'Debian': { + $location = $::operatingsystem ? { + 'Debian' => 'http://downloads-distro.mongodb.org/repo/debian-sysvinit', + 'Ubuntu' => 'http://downloads-distro.mongodb.org/repo/ubuntu-upstart', + default => undef + } + class { 'mongodb::repo::apt': } + } + + default: { + if($ensure == 'present' or $ensure == true) { + fail("Unsupported managed repository for osfamily: ${::osfamily}, operatingsystem: ${::operatingsystem}, module ${module_name} currently only supports managing repos for osfamily RedHat, Debian and Ubuntu") + } + } + } +} diff --git a/mongodb/manifests/repo/apt.pp b/mongodb/manifests/repo/apt.pp new file mode 100644 index 000000000..4f14632d4 --- /dev/null +++ b/mongodb/manifests/repo/apt.pp @@ -0,0 +1,25 @@ +# PRIVATE CLASS: do not use directly +class mongodb::repo::apt inherits mongodb::repo { + # we try to follow/reproduce the instruction + # from http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/ + + include ::apt + + if($::mongodb::repo::ensure == 'present' or $::mongodb::repo::ensure == true) { + apt::source { 'downloads-distro.mongodb.org': + location => $::mongodb::repo::location, + release => 'dist', + repos => '10gen', + key => '9ECBEC467F0CEB10', + key_server => 'hkp://keyserver.ubuntu.com:80', + include_src => false, + } + + Apt::Source['downloads-distro.mongodb.org']->Package<|tag == 'mongodb'|> + } + else { + apt::source { 'downloads-distro.mongodb.org': + ensure => absent, + } + } +} diff --git a/mongodb/manifests/repo/yum.pp b/mongodb/manifests/repo/yum.pp new file mode 100644 index 000000000..3a3f6b5d8 --- /dev/null +++ b/mongodb/manifests/repo/yum.pp @@ -0,0 +1,20 @@ +# PRIVATE CLASS: do not use directly +class mongodb::repo::yum inherits mongodb::repo { + # We try to follow/reproduce the instruction + # http://docs.mongodb.org/manual/tutorial/install-mongodb-on-red-hat-centos-or-fedora-linux/ + + if($::mongodb::repo::ensure == 'present' or $::mongodb::repo::ensure == true) { + yumrepo { 'mongodb': + descr => 'MongoDB/10gen Repository', + baseurl => $::mongodb::repo::location, + gpgcheck => '0', + enabled => '1', + } + Yumrepo['mongodb'] -> Package<|tag == 'mongodb'|> + } + else { + yumrepo { 'mongodb': + enabled => absent, + } + } +} diff --git a/mongodb/manifests/server.pp b/mongodb/manifests/server.pp new file mode 100644 index 000000000..90e785e65 --- /dev/null +++ b/mongodb/manifests/server.pp @@ -0,0 +1,79 @@ +# This installs a MongoDB server. See README.md for more details. +class mongodb::server ( + $ensure = $mongodb::params::ensure, + + $user = $mongodb::params::user, + $group = $mongodb::params::group, + + $config = $mongodb::params::config, + $dbpath = $mongodb::params::dbpath, + $pidfilepath = $mongodb::params::pidfilepath, + + $service_provider = $mongodb::params::service_provider, + $service_name = $mongodb::params::service_name, + $service_enable = $mongodb::params::service_enable, + $service_ensure = $mongodb::params::service_ensure, + $service_status = $mongodb::params::service_status, + + $package_ensure = $ensure, + $package_name = $mongodb::params::server_package_name, + + $logpath = $mongodb::params::logpath, + $bind_ip = $mongodb::params::bind_ip, + $logappend = true, + $fork = $mongodb::params::fork, + $port = 27017, + $journal = $mongodb::params::journal, + $nojournal = undef, + $smallfiles = undef, + $cpu = undef, + $auth = false, + $noauth = undef, + $verbose = undef, + $verbositylevel = undef, + $objcheck = undef, + $quota = undef, + $quotafiles = undef, + $diaglog = undef, + $directoryperdb = undef, + $profile = undef, + $maxconns = undef, + $oplog_size = undef, + $nohints = undef, + $nohttpinterface = undef, + $noscripting = undef, + $notablescan = undef, + $noprealloc = undef, + $nssize = undef, + $mms_token = undef, + $mms_name = undef, + $mms_interval = undef, + $replset = undef, + $rest = undef, + $slowms = undef, + $keyfile = undef, + $set_parameter = undef, + $syslog = undef, + + # Deprecated parameters + $master = undef, + $slave = undef, + $only = undef, + $source = undef, +) inherits mongodb::params { + + + if ($ensure == 'present' or $ensure == true) { + anchor { 'mongodb::server::start': }-> + class { 'mongodb::server::install': }-> + class { 'mongodb::server::config': }-> + class { 'mongodb::server::service': }-> + anchor { 'mongodb::server::end': } + } else { + anchor { 'mongodb::server::start': }-> + class { 'mongodb::server::service': }-> + class { 'mongodb::server::config': }-> + class { 'mongodb::server::install': }-> + anchor { 'mongodb::server::end': } + } +} diff --git a/mongodb/manifests/server/config.pp b/mongodb/manifests/server/config.pp new file mode 100644 index 000000000..2056c14d5 --- /dev/null +++ b/mongodb/manifests/server/config.pp @@ -0,0 +1,92 @@ +# PRIVATE CLASS: do not call directly +class mongodb::server::config { + $ensure = $mongodb::server::ensure + $user = $mongodb::server::user + $group = $mongodb::server::group + $config = $mongodb::server::config + + $dbpath = $mongodb::server::dbpath + $pidfilepath = $mongodb::server::pidfilepath + $logpath = $mongodb::server::logpath + $logappend = $mongodb::server::logappend + $fork = $mongodb::server::fork + $port = $mongodb::server::port + $journal = $mongodb::server::journal + $nojournal = $mongodb::server::nojournal + $smallfiles = $mongodb::server::smallfiles + $cpu = $mongodb::server::cpu + $auth = $mongodb::server::auth + $noath = $mongodb::server::noauth + $verbose = $mongodb::server::verbose + $verbositylevel = $mongodb::server::verbositylevel + $objcheck = $mongodb::server::objcheck + $quota = $mongodb::server::quota + $quotafiles = $mongodb::server::quotafiles + $diaglog = $mongodb::server::diaglog + $oplog_size = $mongodb::server::oplog_size + $nohints = $mongodb::server::nohints + $nohttpinterface = $mongodb::server::nohttpinterface + $noscripting = $mongodb::server::noscripting + $notablescan = $mongodb::server::notablescan + $noprealloc = $mongodb::server::noprealloc + $nssize = $mongodb::server::nssize + $mms_token = $mongodb::server::mms_token + $mms_name = $mongodb::server::mms_name + $mms_interval = $mongodb::server::mms_interval + $master = $mongodb::server::master + $slave = $mongodb::server::slave + $only = $mongodb::server::only + $source = $mongodb::server::source + $replset = $mongodb::server::replset + $rest = $mongodb::server::rest + $slowms = $mongodb::server::slowms + $keyfile = $mongodb::server::keyfile + $bind_ip = $mongodb::server::bind_ip + $directoryperdb = $mongodb::server::directoryperdb + $profile = $mongodb::server::profile + $set_parameter = $mongodb::server::set_parameter + $syslog = $mongodb::server::syslog + + File { + owner => $user, + group => $group, + } + + if ($logpath and $syslog) { fail('You cannot use syslog with logpath')} + + if ($ensure == 'present' or $ensure == true) { + + # Exists for future compatibility and clarity. + if $auth { + $noauth = false + } + else { + $noauth = true + } + + file { $config: + content => template('mongodb/mongodb.conf.erb'), + owner => 'root', + group => 'root', + mode => '0644', + notify => Class['mongodb::server::service'] + } + + file { $dbpath: + ensure => directory, + mode => '0755', + owner => $user, + group => $group, + require => File[$config] + } + } else { + file { $dbpath: + ensure => absent, + force => true, + backup => false, + } + file { $config: + ensure => absent + } + } +} diff --git a/mongodb/manifests/server/install.pp b/mongodb/manifests/server/install.pp new file mode 100644 index 000000000..46b0e749b --- /dev/null +++ b/mongodb/manifests/server/install.pp @@ -0,0 +1,34 @@ +# PRIVATE CLASS: do not call directly +class mongodb::server::install { + $package_ensure = $mongodb::server::package_ensure + $package_name = $mongodb::server::package_name + + case $package_ensure { + true: { + $my_package_ensure = 'present' + $file_ensure = 'directory' + } + false: { + $my_package_ensure = 'absent' + $file_ensure = 'absent' + } + 'absent': { + $my_package_ensure = 'absent' + $file_ensure = 'absent' + } + 'purged': { + $my_package_ensure = 'purged' + $file_ensure = 'absent' + } + default: { + $my_package_ensure = $package_ensure + $file_ensure = 'present' + } + } + + package { 'mongodb_server': + ensure => $my_package_ensure, + name => $package_name, + tag => 'mongodb', + } +} diff --git a/mongodb/manifests/server/service.pp b/mongodb/manifests/server/service.pp new file mode 100644 index 000000000..3a714705e --- /dev/null +++ b/mongodb/manifests/server/service.pp @@ -0,0 +1,34 @@ +# PRIVATE CLASS: do not call directly +class mongodb::server::service { + $ensure = $mongodb::server::service_ensure + $service_enable = $mongodb::server::service_enable + $service_name = $mongodb::server::service_name + $service_provider = $mongodb::server::service_provider + $service_status = $mongodb::server::service_status + $bind_ip = $mongodb::server::bind_ip + $port = $mongodb::server::port + + $service_ensure = $ensure ? { + present => true, + absent => false, + purged => false, + default => $ensure + } + + service { 'mongodb': + ensure => $service_ensure, + name => $service_name, + enable => $service_enable, + provider => $service_provider, + hasstatus => true, + status => $service_status, + } + if $service_ensure { + mongodb_conn_validator { "mongodb": + server => $bind_ip, + port => $port, + timeout => '240', + require => Service['mongodb'], + } + } +} diff --git a/mongodb/spec/acceptance/nodesets/centos-6-vcloud.yml b/mongodb/spec/acceptance/nodesets/centos-6-vcloud.yml new file mode 100644 index 000000000..ae19ee77c --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/centos-6-vcloud.yml @@ -0,0 +1,21 @@ +HOSTS: + 'master': + roles: + - master + platform: el-6-x86_64 + hypervisor: vcloud + template: centos-6-x86_64 + 'slave': + roles: + - slave + platform: el-6-x86_64 + hypervisor: vcloud + template: centos-6-x86_64 +CONFIG: + type: foss + ssh: + keys: "~/.ssh/id_rsa-acceptance" + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/mongodb/spec/acceptance/nodesets/centos-64-x64-pe.yml b/mongodb/spec/acceptance/nodesets/centos-64-x64-pe.yml new file mode 100644 index 000000000..7d9242f1b --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/centos-64-x64-pe.yml @@ -0,0 +1,12 @@ +HOSTS: + centos-64-x64: + roles: + - master + - database + - dashboard + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: pe diff --git a/mongodb/spec/acceptance/nodesets/centos-64-x64.yml b/mongodb/spec/acceptance/nodesets/centos-64-x64.yml new file mode 100644 index 000000000..05540ed8c --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/centos-64-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-64-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mongodb/spec/acceptance/nodesets/debian-607-x64.yml b/mongodb/spec/acceptance/nodesets/debian-607-x64.yml new file mode 100644 index 000000000..4668f5f9d --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/debian-607-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-607-x64: + roles: + - master + platform: debian-6-amd64 + box : debian-607-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-607-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git \ No newline at end of file diff --git a/mongodb/spec/acceptance/nodesets/debian-70rc1-x64.yml b/mongodb/spec/acceptance/nodesets/debian-70rc1-x64.yml new file mode 100644 index 000000000..750d299eb --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/debian-70rc1-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-70rc1-x64: + roles: + - master + platform: debian-7-amd64 + box : debian-70rc1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-70rc1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git \ No newline at end of file diff --git a/mongodb/spec/acceptance/nodesets/default.yml b/mongodb/spec/acceptance/nodesets/default.yml new file mode 100644 index 000000000..4e2cb809e --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/default.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-65-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-65-x64-vbox436-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-65-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mongodb/spec/acceptance/nodesets/fedora-18-x64.yml b/mongodb/spec/acceptance/nodesets/fedora-18-x64.yml new file mode 100644 index 000000000..136164983 --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/fedora-18-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + fedora-18-x64: + roles: + - master + platform: fedora-18-x86_64 + box : fedora-18-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/fedora-18-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mongodb/spec/acceptance/nodesets/multi-centos-6-vcloud.yml b/mongodb/spec/acceptance/nodesets/multi-centos-6-vcloud.yml new file mode 100644 index 000000000..ae19ee77c --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/multi-centos-6-vcloud.yml @@ -0,0 +1,21 @@ +HOSTS: + 'master': + roles: + - master + platform: el-6-x86_64 + hypervisor: vcloud + template: centos-6-x86_64 + 'slave': + roles: + - slave + platform: el-6-x86_64 + hypervisor: vcloud + template: centos-6-x86_64 +CONFIG: + type: foss + ssh: + keys: "~/.ssh/id_rsa-acceptance" + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/mongodb/spec/acceptance/nodesets/multi-centos-64-x64.yml b/mongodb/spec/acceptance/nodesets/multi-centos-64-x64.yml new file mode 100644 index 000000000..5bcd4779d --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/multi-centos-64-x64.yml @@ -0,0 +1,17 @@ +HOSTS: + 'master': + roles: + - master + platform: el-6-x86_64 + hypervisor : vagrant + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + 'slave': + roles: + - master + platform: el-6-x86_64 + hypervisor : vagrant + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box +CONFIG: + type: foss diff --git a/mongodb/spec/acceptance/nodesets/sles-11-x64.yml b/mongodb/spec/acceptance/nodesets/sles-11-x64.yml new file mode 100644 index 000000000..41abe2135 --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/sles-11-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + sles-11-x64.local: + roles: + - master + platform: sles-11-x64 + box : sles-11sp1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/sles-11sp1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mongodb/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml b/mongodb/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml new file mode 100644 index 000000000..5ca1514e4 --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-10044-x64: + roles: + - master + platform: ubuntu-10.04-amd64 + box : ubuntu-server-10044-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-10044-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mongodb/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml b/mongodb/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml new file mode 100644 index 000000000..d065b304f --- /dev/null +++ b/mongodb/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-12042-x64: + roles: + - master + platform: ubuntu-12.04-amd64 + box : ubuntu-server-12042-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mongodb/spec/acceptance/replset_spec.rb b/mongodb/spec/acceptance/replset_spec.rb new file mode 100644 index 000000000..8be5a0f46 --- /dev/null +++ b/mongodb/spec/acceptance/replset_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper_acceptance' + +if hosts.length > 1 + describe 'mongodb_replset resource' do + after :all do + # Have to drop the DB to disable replsets for further testing + on hosts, %{mongo local --verbose --eval 'db.dropDatabase()'} + + pp = <<-EOS + class { 'mongodb::globals': } + -> class { 'mongodb::server': + ensure => purged, + } + if $::osfamily == 'RedHat' { + class { 'mongodb::client': } + } + EOS + + apply_manifest_on(hosts.reverse, pp, :catch_failures => true) + end + + it 'configures mongo on both nodes' do + pp = <<-EOS + class { 'mongodb::globals': } + -> class { 'mongodb::server': + bind_ip => '0.0.0.0', + replset => 'test', + } + if $::osfamily == 'RedHat' { + class { 'mongodb::client': } + } + EOS + + apply_manifest_on(hosts.reverse, pp, :catch_failures => true) + apply_manifest_on(hosts.reverse, pp, :catch_changes => true) + end + + it 'sets up the replset with puppet' do + pp = <<-EOS + mongodb_replset { 'test': + members => [#{hosts.collect{|x|"'#{x}:27017'"}.join(',')}], + } + EOS + apply_manifest_on(hosts_as('master'), pp, :catch_failures => true) + on(hosts_as('master'), 'mongo --quiet --eval "printjson(rs.conf())"') do |r| + expect(r.stdout).to match /#{hosts[0]}:27017/ + expect(r.stdout).to match /#{hosts[1]}:27017/ + end + end + + it 'inserts data on the master' do + sleep(30) + on hosts_as('master'), %{mongo --verbose --eval 'db.test.save({name:"test1",value:"some value"})'} + end + + it 'checks the data on the master' do + on hosts_as('master'), %{mongo --verbose --eval 'printjson(db.test.findOne({name:"test1"}))'} do |r| + expect(r.stdout).to match /some value/ + end + end + + it 'checks the data on the slave' do + sleep(10) + on hosts_as('slave'), %{mongo --verbose --eval 'rs.slaveOk(); printjson(db.test.findOne({name:"test1"}))'} do |r| + expect(r.stdout).to match /some value/ + end + end + end +end diff --git a/mongodb/spec/acceptance/server_spec.rb b/mongodb/spec/acceptance/server_spec.rb new file mode 100644 index 000000000..a313b9885 --- /dev/null +++ b/mongodb/spec/acceptance/server_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper_acceptance' + +describe 'mongodb::server class' do + + shared_examples 'normal tests' do |tengen| + if tengen + case fact('osfamily') + when 'RedHat' + package_name = 'mongodb-org-server' + service_name = 'mongod' + config_file = '/etc/mongod.conf' + when 'Debian' + package_name = 'mongodb-org-server' + service_name = 'mongod' + config_file = '/etc/mongod.conf' + end + else + case fact('osfamily') + when 'RedHat' + package_name = 'mongodb-server' + service_name = 'mongod' + config_file = '/etc/mongodb.conf' + when 'Debian' + package_name = 'mongodb-server' + service_name = 'mongodb' + config_file = '/etc/mongodb.conf' + end + end + + client_name = 'mongo --version' + + context "default parameters with 10gen => #{tengen}" do + after :all do + if tengen + puts "XXX uninstalls mongodb because changing the port with tengen doesn't work because they have a crappy init script" + pp = <<-EOS + class {'mongodb::globals': manage_package_repo => #{tengen}, } + -> class { 'mongodb::server': ensure => absent, } + -> class { 'mongodb::client': ensure => absent, } + EOS + apply_manifest(pp, :catch_failures => true) + end + end + + it 'should work with no errors' do + pp = <<-EOS + class { 'mongodb::globals': manage_package_repo => #{tengen}, } + -> class { 'mongodb::server': } + -> class { 'mongodb::client': } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + describe package(package_name) do + it { should be_installed } + end + + describe file(config_file) do + it { should be_file } + end + + describe service(service_name) do + it { should be_enabled } + it { should be_running } + end + + describe port(27017) do + it { should be_listening } + end + + describe command(client_name) do + it do + should return_exit_status 0 + end + end + end + + context "test using custom port and 10gen => #{tengen}" do + it 'change port to 27018' do + pp = <<-EOS + class { 'mongodb::globals': manage_package_repo => #{tengen}, } + -> class { 'mongodb::server': port => 27018, } + -> class { 'mongodb::client': } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + describe port(27018) do + it { should be_listening } + end + end + + describe "uninstalling with 10gen => #{tengen}" do + it 'uninstalls mongodb' do + pp = <<-EOS + class {'mongodb::globals': manage_package_repo => #{tengen}, } + -> class { 'mongodb::server': ensure => absent, } + -> class { 'mongodb::client': ensure => absent, } + EOS + apply_manifest(pp, :catch_failures => true) + end + end + end + + it_behaves_like 'normal tests', false + it_behaves_like 'normal tests', true +end diff --git a/mongodb/spec/classes/client_install_spec.rb b/mongodb/spec/classes/client_install_spec.rb new file mode 100644 index 000000000..5fe4bfa7c --- /dev/null +++ b/mongodb/spec/classes/client_install_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe 'mongodb::client::install', :type => :class do + describe 'it should create package' do + let(:pre_condition) { ["class mongodb::client { $ensure = true $package_name = 'mongodb' }", "include mongodb::client"]} + it { + should contain_package('mongodb_client').with({ + :ensure => 'present', + :name => 'mongodb', + }) + } + end +end diff --git a/mongodb/spec/classes/repo_spec.rb b/mongodb/spec/classes/repo_spec.rb new file mode 100644 index 000000000..aa051e915 --- /dev/null +++ b/mongodb/spec/classes/repo_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'mongodb::repo', :type => :class do + + context 'when deploying on Debian' do + let :facts do + { + :osfamily => 'Debian', + :operatingsystem => 'Debian', + :lsbdistid => 'Debian', + } + end + + it { + should contain_class('mongodb::repo::apt') + } + end + + context 'when deploying on CentOS' do + let :facts do + { + :osfamily => 'RedHat', + :operatingsystem => 'CentOS', + } + end + + it { + should contain_class('mongodb::repo::yum') + } + end + +end diff --git a/mongodb/spec/classes/server_config_spec.rb b/mongodb/spec/classes/server_config_spec.rb new file mode 100644 index 000000000..db05b88e3 --- /dev/null +++ b/mongodb/spec/classes/server_config_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe 'mongodb::server::config', :type => :class do + + describe 'with preseted variables' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' }", "include mongodb::server"]} + + it { + should contain_file('/etc/mongod.conf') + } + + end + + describe 'with default values' do + let(:pre_condition) {[ "class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' $ensure = present $user = 'mongod' $group = 'mongod' $port = 29017 $bind_ip = ['0.0.0.0'] $fork = true $logpath ='/var/log/mongo/mongod.log' $logappend = true }", "include mongodb::server" ]} + + it { + should contain_file('/etc/mongod.conf').with({ + :mode => '0644', + :owner => 'root', + :group => 'root' + }) + + should contain_file('/etc/mongod.conf').with_content(/^dbpath=\/var\/lib\/mongo/) + should contain_file('/etc/mongod.conf').with_content(/bind_ip\s=\s0\.0\.0\.0/) + should contain_file('/etc/mongod.conf').with_content(/^port = 29017$/) + should contain_file('/etc/mongod.conf').with_content(/^logappend=true/) + should contain_file('/etc/mongod.conf').with_content(/^logpath=\/var\/log\/mongo\/mongod\.log/) + should contain_file('/etc/mongod.conf').with_content(/^fork=true/) + } + end + + describe 'with absent ensure' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' $ensure = absent }", "include mongodb::server"]} + + it { + should contain_file('/etc/mongod.conf').with({ :ensure => 'absent' }) + } + + end + + describe 'with specific bind_ip values' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' $ensure = present $bind_ip = ['127.0.0.1', '10.1.1.13']}", "include mongodb::server"]} + + it { + should contain_file('/etc/mongod.conf').with_content(/bind_ip\s=\s127\.0\.0\.1\,10\.1\.1\.13/) + } + end + + describe 'when specifying auth to true' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $auth = true $dbpath = '/var/lib/mongo' $ensure = present }", "include mongodb::server"]} + + it { + should contain_file('/etc/mongod.conf').with_content(/^auth=true/) + } + end + + describe 'when specifying set_parameter value' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $set_parameter = 'textSearchEnable=true' $dbpath = '/var/lib/mongo' $ensure = present }", "include mongodb::server"]} + + it { + should contain_file('/etc/mongod.conf').with_content(/^setParameter = textSearchEnable=true/) + } + end + + describe 'with journal:' do + context 'on true with i686 architecture' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' $ensure = present $journal = true }", "include mongodb::server"]} + let (:facts) { { :architecture => 'i686' } } + + it { + should contain_file('/etc/mongod.conf').with_content(/^journal = true/) + } + end + end + + # check nested quota and quotafiles + describe 'with quota to' do + + context 'true and without quotafiles' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' $ensure = present $quota = true }", "include mongodb::server"]} + it { + should contain_file('/etc/mongod.conf').with_content(/^quota = true/) + } + end + + context 'true and with quotafiles' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' $ensure = present $quota = true $quotafiles = 1 }", "include mongodb::server"]} + + it { + should contain_file('/etc/mongod.conf').with_content(/quota = true/) + should contain_file('/etc/mongod.conf').with_content(/quotaFiles = 1/) + } + end + end + + describe 'when specifying syslog value' do + context 'it should be set to true' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' $ensure = present $syslog = true }", "include mongodb::server"]} + + it { + should contain_file('/etc/mongod.conf').with_content(/^syslog = true/) + } + end + + context 'if logpath is also set an error should be raised' do + let(:pre_condition) { ["class mongodb::server { $config = '/etc/mongod.conf' $dbpath = '/var/lib/mongo' $ensure = present $syslog = true $logpath ='/var/log/mongo/mongod.log' }", "include mongodb::server"]} + + it { + expect { should contain_file('/etc/mongod.conf') }.to raise_error(Puppet::Error, /You cannot use syslog with logpath/) + } + end + + end + +end diff --git a/mongodb/spec/classes/server_install_spec.rb b/mongodb/spec/classes/server_install_spec.rb new file mode 100644 index 000000000..5ca01a90e --- /dev/null +++ b/mongodb/spec/classes/server_install_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'mongodb::server::install', :type => :class do + + describe 'it should create package and dbpath file' do + let(:pre_condition) { ["class mongodb::server { $package_ensure = true $dbpath = '/var/lib/mongo' $user = 'mongodb' $package_name = 'mongodb-server' }", "include mongodb::server"]} + + it { + should contain_package('mongodb_server').with({ + :ensure => 'present', + :name => 'mongodb-server', + }) + } + end + +end diff --git a/mongodb/spec/classes/server_spec.rb b/mongodb/spec/classes/server_spec.rb new file mode 100644 index 000000000..c74e7f0c1 --- /dev/null +++ b/mongodb/spec/classes/server_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'mongodb::server' do + let :facts do + { + :osfamily => 'Debian', + :operatingsystem => 'Debian', + } + end + + context 'with defaults' do + it { should contain_class('mongodb::server::install') } + it { should contain_class('mongodb::server::config') } + end + + context 'when deploying on Solaris' do + let :facts do + { :osfamily => 'Solaris' } + end + it { expect { should raise_error(Puppet::Error) } } + end + +end \ No newline at end of file diff --git a/mongodb/spec/defines/db_spec.rb b/mongodb/spec/defines/db_spec.rb new file mode 100644 index 000000000..65a6f1052 --- /dev/null +++ b/mongodb/spec/defines/db_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'mongodb::db', :type => :define do + let(:title) { 'testdb' } + + let(:params) { + { 'user' => 'testuser', + 'password' => 'testpass', + } + } + + it 'should contain mongodb_database with mongodb::server requirement' do + should contain_mongodb_database('testdb')\ + .with_require('Class[Mongodb::Server]') + end + + it 'should contain mongodb_user with mongodb_database requirement' do + should contain_mongodb_user('testuser')\ + .with_require('Mongodb_database[testdb]') + end + + it 'should contain mongodb_user with proper database name' do + should contain_mongodb_user('testuser')\ + .with_database('testdb') + end + + it 'should contain mongodb_user with proper roles' do + params.merge!({'roles' => ['testrole1', 'testrole2']}) + should contain_mongodb_user('testuser')\ + .with_roles(["testrole1", "testrole2"]) + end + + it 'should prefer password_hash instead of password' do + params.merge!({'password_hash' => 'securehash'}) + should contain_mongodb_user('testuser')\ + .with_password_hash('securehash') + end + + it 'should contain mongodb_database with proper tries param' do + params.merge!({'tries' => 5}) + should contain_mongodb_database('testdb').with_tries(5) + end +end diff --git a/mongodb/spec/spec_helper.rb b/mongodb/spec/spec_helper.rb new file mode 100644 index 000000000..2c6f56649 --- /dev/null +++ b/mongodb/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/mongodb/spec/spec_helper_acceptance.rb b/mongodb/spec/spec_helper_acceptance.rb new file mode 100755 index 000000000..5ea038b34 --- /dev/null +++ b/mongodb/spec/spec_helper_acceptance.rb @@ -0,0 +1,41 @@ +#! /usr/bin/env ruby -S rspec +require 'beaker-rspec' + +UNSUPPORTED_PLATFORMS = [] + +unless ENV['RS_PROVISION'] == 'no' or ENV['BEAKER_provision'] == 'no' + if hosts.first.is_pe? + install_pe + on hosts, 'mkdir -p /etc/puppetlabs/facter/facts.d' + else + install_puppet + on hosts, 'mkdir -p /etc/facter/facts.d' + on hosts, '/bin/touch /etc/puppet/hiera.yaml' + end + hosts.each do |host| + on host, "mkdir -p #{host['distmoduledir']}" + end +end + +RSpec.configure do |c| + # Project root + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Readable test descriptions + c.formatter = :documentation + + # Configure all nodes in nodeset + c.before :suite do + hosts.each do |host| + copy_module_to(host, :source => proj_root, :module_name => 'mongodb') + end + on hosts, 'puppet module install puppetlabs-stdlib' + on hosts, 'puppet module install puppetlabs-apt' + case fact('osfamily') + when 'RedHat' + on hosts, 'puppet module install stahnma-epel' + apply_manifest_on hosts, 'include epel' + on hosts, 'service iptables stop' + end + end +end diff --git a/mongodb/spec/spec_helper_system.rb b/mongodb/spec/spec_helper_system.rb new file mode 100644 index 000000000..7e2c48fb5 --- /dev/null +++ b/mongodb/spec/spec_helper_system.rb @@ -0,0 +1,34 @@ +require 'rspec-system/spec_helper' +require 'rspec-system-puppet/helpers' +require 'rspec-system-serverspec/helpers' + +include RSpecSystemPuppet::Helpers +include Serverspec::Helper::RSpecSystem +include Serverspec::Helper::DetectOS + +RSpec.configure do |c| + # Project root + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Enable colour + c.tty = true + + c.include RSpecSystemPuppet::Helpers + + # This is where we 'setup' the nodes before running our tests + c.before :suite do + # Install puppet + puppet_install + + # Install modules and dependencies + puppet_module_install(:source => proj_root, :module_name => 'mongodb') + shell('puppet module install puppetlabs-stdlib') + shell('puppet module install puppetlabs-apt') + + case node.facts['osfamily'] + when 'RedHat' + shell('puppet module install stahnma-epel') + puppet_apply('include epel') + end + end +end diff --git a/mongodb/spec/system/basic_spec.rb b/mongodb/spec/system/basic_spec.rb new file mode 100644 index 000000000..300835635 --- /dev/null +++ b/mongodb/spec/system/basic_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper_system' + +describe 'basic tests:' do + # Using puppet_apply as a subject + context puppet_apply 'notice("foo")' do + its(:stdout) { should =~ /foo/ } + its(:stderr) { should be_empty } + its(:exit_code) { should be_zero } + end +end diff --git a/mongodb/spec/system/server_10gen_spec.rb b/mongodb/spec/system/server_10gen_spec.rb new file mode 100644 index 000000000..a9a6b86d3 --- /dev/null +++ b/mongodb/spec/system/server_10gen_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper_system' + +describe 'mongodb::server: with 10gen repo' do + + case node.facts['osfamily'] + when 'RedHat' + package_name = 'mongo-10gen-server' + service_name = 'mongod' + config_file = '/etc/mongod.conf' + when 'Debian' + package_name = 'mongodb-10gen' + service_name = 'mongodb' + config_file = '/etc/mongodb.conf' + end + + context 'default parameters' do + it 'should work with no errors' do + pp = <<-EOS + class { 'mongodb::globals': manage_package_repo => true }-> + class { 'mongodb::server': } + EOS + + puppet_apply(pp) do |r| + r.exit_code.should == 2 + r.refresh + r.exit_code.should == 0 + end + end + + describe package(package_name) do + it { should be_installed } + end + + describe file(config_file) do + it { should be_file } + end + + describe service(service_name) do + it { should be_enabled } + it { should be_running } + end + + describe port(27017) do + it do + should be_listening + end + end + end + + context 'test using custom port' do + + it 'change port to 27018' do + pp = <<-EOS + class { 'mongodb::globals': manage_package_repo => true }-> + class { 'mongodb::server': port => 27018 } + EOS + + puppet_apply(pp) do |r| + r.exit_code.should == 2 + r.refresh + r.exit_code.should == 0 + end + end + + describe port(27018) do + it { should be_listening } + end + end + + describe 'cleanup' do + it 'uninstalls mongodb' do + puppet_apply("class {'mongodb::globals': manage_package_repo => true }-> class { 'mongodb::server': ensure => false }") do |r| + r.exit_code.should_not == 1 + end + end + end +end diff --git a/mongodb/spec/system/server_distro_spec.rb b/mongodb/spec/system/server_distro_spec.rb new file mode 100644 index 000000000..6ed97d088 --- /dev/null +++ b/mongodb/spec/system/server_distro_spec.rb @@ -0,0 +1,71 @@ +begin +require 'spec_helper_system' + +describe 'mongodb::server:' do + + case node.facts['osfamily'] + when 'RedHat' + package_name = 'mongodb-server' + service_name = 'mongod' + config_file = '/etc/mongodb.conf' + when 'Debian' + package_name = 'mongodb-server' + service_name = 'mongodb' + config_file = '/etc/mongodb.conf' + end + + context 'default parameters' do + it 'should work with no errors' do + pp = <<-EOS + class { 'mongodb::server': } + EOS + + puppet_apply(pp) do |r| + r.exit_code.should == 2 + r.refresh + r.exit_code.should == 0 + end + end + + describe package(package_name) do + it { should be_installed } + end + + describe file(config_file) do + it { should be_file } + end + + describe service(service_name) do + it { should be_enabled } + it { should be_running } + end + + describe port(27017) do + it do + sleep(20) + should be_listening + end + end + end + + context 'test using custom port' do + it 'change port to 27018' do + pp = <<-EOS + class { 'mongodb::server': port => 27018 } + EOS + + puppet_apply(pp) do |r| + r.exit_code.should == 2 + r.refresh + r.exit_code.should == 0 + end + end + + describe port(27018) do + it { should be_listening } + end + + end +end + +end diff --git a/mongodb/spec/unit/mongodb_password_spec.rb b/mongodb/spec/unit/mongodb_password_spec.rb new file mode 100644 index 000000000..5b0b825e5 --- /dev/null +++ b/mongodb/spec/unit/mongodb_password_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe 'the mongodb_password function' do + before :all do + Puppet::Parser::Functions.autoloader.loadall + end + + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it 'should exist' do + Puppet::Parser::Functions.function('mongodb_password').should == 'function_mongodb_password' + end + + it 'should raise a ParseError if there no arguments' do + lambda { scope.function_mongodb_password([]) }.should( raise_error(Puppet::ParseError)) + end + + it 'should raise a ParseError if there is more than 2 arguments' do + lambda { scope.function_mongodb_password(%w(foo bar baz)) }.should( raise_error(Puppet::ParseError)) + end + + it 'should convert password into a hash' do + result = scope.function_mongodb_password(%w(user pass)) + result.should(eq('e0c4a7b97d4db31f5014e9694e567d6b')) + end + +end diff --git a/mongodb/spec/unit/puppet/provider/mongodb_database/mongodb_spec.rb b/mongodb/spec/unit/puppet/provider/mongodb_database/mongodb_spec.rb new file mode 100644 index 000000000..91cbe8c5f --- /dev/null +++ b/mongodb/spec/unit/puppet/provider/mongodb_database/mongodb_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Puppet::Type.type(:mongodb_database).provider(:mongodb) do + + let(:resource) { Puppet::Type.type(:mongodb_database).new( + { :ensure => :present, + :name => 'new_database', + :provider => described_class.name + } + )} + + let(:provider) { resource.provider } + + describe 'create' do + it 'makes a database' do + provider.expects(:mongo) + provider.create + end + end + + describe 'destroy' do + it 'removes a database' do + provider.expects(:mongo) + provider.destroy + end + end + + describe 'exists?' do + it 'checks if database exists' do + provider.expects(:mongo).at_least(2).returns("db1,new_database,db2") + provider.exists?.should eql true + end + end + +end diff --git a/mongodb/spec/unit/puppet/provider/mongodb_replset/mongodb_spec.rb b/mongodb/spec/unit/puppet/provider/mongodb_replset/mongodb_spec.rb new file mode 100644 index 000000000..d0c2d128e --- /dev/null +++ b/mongodb/spec/unit/puppet/provider/mongodb_replset/mongodb_spec.rb @@ -0,0 +1,148 @@ +# +# Authors: Emilien Macchi +# Francois Charlier +# + +require 'spec_helper' + +describe Puppet::Type.type(:mongodb_replset).provider(:mongo) do + + valid_members = ['mongo1:27017', 'mongo2:27017', 'mongo3:27017'] + + let(:resource) { Puppet::Type.type(:mongodb_replset).new( + { :ensure => :present, + :name => 'rs_test', + :members => valid_members, + :provider => :mongo + } + )} + + let(:resources) { { 'rs_test' => resource } } + let(:provider) { described_class.new(resource) } + + describe '#create' do + it 'should create a replicaset' do + provider.class.stubs(:get_replset_properties) + provider.stubs(:alive_members).returns(valid_members) + provider.expects('rs_initiate').with("{ _id: \"rs_test\", members: [ { _id: 0, host: \"mongo1:27017\" },{ _id: 1, host: \"mongo2:27017\" },{ _id: 2, host: \"mongo3:27017\" } ] }", "mongo1:27017").returns( + { "info" => "Config now saved locally. Should come online in about a minute.", + "ok" => 1 } ) + provider.create + provider.flush + end + end + + describe '#exists?' do + describe 'when the replicaset does not exist' do + it 'returns false' do + provider.class.stubs(:mongo).returns(< "rs_test" }) + provider.expects('rs_add').times(2).returns({ 'ok' => 1 }) + provider.members=(valid_members) + provider.flush + end + + it 'raises an error when the master host is not available' do + provider.stubs(:rs_status).returns({ "set" => "rs_test" }) + provider.stubs(:db_ismaster).returns({ "primary" => false }) + provider.members=(valid_members) + expect { provider.flush }.to raise_error(Puppet::Error, "Can't find master host for replicaset #{resource[:name]}.") + end + + it 'raises an error when at least one member is not running with --replSet' do + provider.stubs(:rs_status).returns({ "ok" => 0, "errmsg" => "not running with --replSet" }) + provider.members=(valid_members) + expect { provider.flush }.to raise_error(Puppet::Error, /is not supposed to be part of a replicaset\.$/) + end + + it 'raises an error when at least one member is configured with another replicaset name' do + provider.stubs(:rs_status).returns({ "set" => "rs_another" }) + provider.members=(valid_members) + expect { provider.flush }.to raise_error(Puppet::Error, /is already part of another replicaset\.$/) + end + + it 'raises an error when no member is available' do + provider.class.stubs(:mongo_command).raises(Puppet::ExecutionFailure, < :present, + :name => 'new_user', + :database => 'new_database', + :password_hash => 'pass', + :roles => ['role1', 'role2'], + :provider => described_class.name + } + )} + + let(:provider) { resource.provider } + + describe 'create' do + it 'creates a user' do + provider.expects(:mongo) + provider.create + end + end + + describe 'destroy' do + it 'removes a user' do + provider.expects(:mongo) + provider.destroy + end + end + + describe 'exists?' do + it 'checks if user exists' do + provider.expects(:mongo).at_least(2).returns("1") + provider.exists?.should eql true + end + end + + describe 'password_hash' do + it 'returns a password_hash' do + provider.expects(:mongo).returns("pass\n") + provider.password_hash.should == "pass" + end + end + + describe 'password_hash=' do + it 'changes a password_hash' do + provider.expects(:mongo) + provider.password_hash=("newpass") + end + end + + describe 'roles' do + it 'returns a sorted roles' do + provider.expects(:mongo).returns("role2,role1\n") + provider.roles.should == ['role1','role2'] + end + end + + describe 'roles=' do + it 'changes a roles' do + provider.expects(:mongo) + provider.roles=(['role3','role4']) + end + end + +end diff --git a/mongodb/spec/unit/puppet/type/mongodb_database_spec.rb b/mongodb/spec/unit/puppet/type/mongodb_database_spec.rb new file mode 100644 index 000000000..245a1becf --- /dev/null +++ b/mongodb/spec/unit/puppet/type/mongodb_database_spec.rb @@ -0,0 +1,24 @@ +require 'puppet' +require 'puppet/type/mongodb_database' +describe Puppet::Type.type(:mongodb_database) do + + before :each do + @db = Puppet::Type.type(:mongodb_database).new(:name => 'test') + end + + it 'should accept a database name' do + @db[:name].should == 'test' + end + + it 'should accept a tries parameter' do + @db[:tries] = 5 + @db[:tries].should == 5 + end + + it 'should require a name' do + expect { + Puppet::Type.type(:mongodb_database).new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + +end diff --git a/mongodb/spec/unit/puppet/type/mongodb_replset_spec.rb b/mongodb/spec/unit/puppet/type/mongodb_replset_spec.rb new file mode 100644 index 000000000..f9b72d423 --- /dev/null +++ b/mongodb/spec/unit/puppet/type/mongodb_replset_spec.rb @@ -0,0 +1,28 @@ +# +# Author: Emilien Macchi +# + +require 'puppet' +require 'puppet/type/mongodb_replset' +describe Puppet::Type.type(:mongodb_replset) do + + before :each do + @replset = Puppet::Type.type(:mongodb_replset).new(:name => 'test') + end + + it 'should accept a replica set name' do + @replset[:name].should == 'test' + end + + it 'should accept a members array' do + @replset[:members] = ['mongo1:27017', 'mongo2:27017'] + @replset[:members].should == ['mongo1:27017', 'mongo2:27017'] + end + + it 'should require a name' do + expect { + Puppet::Type.type(:mongodb_replset).new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + +end diff --git a/mongodb/spec/unit/puppet/type/mongodb_user_spec.rb b/mongodb/spec/unit/puppet/type/mongodb_user_spec.rb new file mode 100644 index 000000000..c822265d1 --- /dev/null +++ b/mongodb/spec/unit/puppet/type/mongodb_user_spec.rb @@ -0,0 +1,67 @@ +require 'puppet' +require 'puppet/type/mongodb_user' +describe Puppet::Type.type(:mongodb_user) do + + before :each do + @user = Puppet::Type.type(:mongodb_user).new( + :name => 'test', + :database => 'testdb', + :password_hash => 'pass') + end + + it 'should accept a user name' do + @user[:name].should == 'test' + end + + it 'should accept a database name' do + @user[:database].should == 'testdb' + end + + it 'should accept a tries parameter' do + @user[:tries] = 5 + @user[:tries].should == 5 + end + + it 'should accept a password' do + @user[:password_hash] = 'foo' + @user[:password_hash].should == 'foo' + end + + it 'should use default role' do + @user[:roles].should == ['dbAdmin'] + end + + it 'should accept a roles array' do + @user[:roles] = ['role1', 'role2'] + @user[:roles].should == ['role1', 'role2'] + end + + it 'should require a name' do + expect { + Puppet::Type.type(:mongodb_user).new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + + it 'should require a database' do + expect { + Puppet::Type.type(:mongodb_user).new({:name => 'test', :password_hash => 'pass'}) + }.to raise_error(Puppet::Error, 'Parameter \'database\' must be set') + end + + it 'should require a password_hash' do + expect { + Puppet::Type.type(:mongodb_user).new({:name => 'test', :database => 'testdb'}) + }.to raise_error(Puppet::Error, 'Property \'password_hash\' must be set. Use mongodb_password() for creating hash.') + end + + it 'should sort roles' do + # Reinitialize type with explicit unsorted roles. + @user = Puppet::Type.type(:mongodb_user).new( + :name => 'test', + :database => 'testdb', + :password_hash => 'pass', + :roles => ['b', 'a']) + @user[:roles].should == ['a', 'b'] + end + +end diff --git a/mongodb/templates/mongodb.conf.erb b/mongodb/templates/mongodb.conf.erb new file mode 100644 index 000000000..8e6dea0c9 --- /dev/null +++ b/mongodb/templates/mongodb.conf.erb @@ -0,0 +1,169 @@ +# mongo.conf - generated from Puppet + + +<% if @logpath -%> +#where to log +logpath=<%= @logpath %> +<% if @logappend -%> +logappend=<%= @logappend %> +<% end -%> +<% end -%> +<% if @bind_ip -%> +# Set this option to configure the mongod or mongos process to bind to and +# listen for connections from applications on this address. +# You may concatenate a list of comma separated values to bind mongod to multiple IP addresses. +bind_ip = <%= Array(@bind_ip).join(',') %> +<% end -%> +<% if @fork -%> +# fork and run in background +fork=<%= @fork %> +<% end -%> +port = <%= @port %> +dbpath=<%= @dbpath %> +<% if @pidfilepath -%> +# location of pidfile +pidfilepath=<%= @pidfilepath %> +<% end -%> +<% if @nojournal and not @journal -%> +# Disables write-ahead journaling +nojournal = <%= @nojournal %> +<% end -%> +<% if @journal and not @nojournal -%> +# Enables journaling +journal = <%= @journal %> +<% end -%> +<% if @cpu -%> +# Enables periodic logging of CPU utilization and I/O wait +cpu = <%= @cpu %> +<% end -%> +# Turn on/off security. Off is currently the default +<% if @noauth and not @auth -%> +noauth=<%= @noauth %> +<% end -%> +<% if @auth and not @noauth -%> +auth=<%= @auth %> +<% end -%> +<% if @verbose -%> +# Verbose logging output. +verbose = <%= @verbose %> +<% end -%> +<% if @verbositylevel -%> +<%= @verbositylevel -%> = true +<% end -%> +<% if @objcheck -%> +# Inspect all client data for validity on receipt (useful for +# developing drivers) +objcheck = <%= @objcheck %> +<% end -%> +<% if @maxconns -%> +maxConns = <%= @maxconns %> +<% end -%> +<% if @quota -%> +# Enable db quota management +quota = <%= @quota %> +<% if @quotafiles -%> +quotaFiles = <%= @quotafiles %> +<% end -%> +<% end -%> +<% if @diaglog -%> +# Set oplogging level where n is +# 0=off (default) +# 1=W +# 2=R +# 3=both +# 7=W+some reads +diaglog = <%= @diaglog %> +<% end -%> +<% if @oplog_size -%> +# Specifies a maximum size in megabytes for the replication operation log +oplogSize = <%= @oplog_size %> +<% end -%> +<% if @nohints -%> +# Ignore query hints +nohints = <%= @nohints %> +<% end -%> +<% if @nohttpinterface -%> +# Disable the HTTP interface (Defaults to localhost:27018). +nohttpinterface = <%= @nohttpinterface %> +<% end -%> +<% if @noscripting -%> +# Turns off server-side scripting. This will result in greatly limited +# functionality +noscripting = <%= @noscripting %> +<% end -%> +<% if @notablescan -%> +# Turns off table scans. Any query that would do a table scan fails. +notablescan = <%= @notablescan %> +<% end -%> +<% if @noprealloc -%> +# Disable data file preallocation. +noprealloc = <%= @noprealloc %> +<% end -%> +<% if @nssize -%> +# Specify .ns file size for new databases in megabytes. +nssize = <%= @nssize %> +<% end -%> +<% if @mms_token -%> +# Accout token for Mongo monitoring server. +mms-token = <%= @mms_token %> +<% end -%> +<% if @mms_name -%> +# Server name for Mongo monitoring server. +mms-name = <%= @mms_name %> +<% end -%> +<% if @mms_interval -%> +# Ping interval for Mongo monitoring server. +mms-interval = <%= @mms_interval %> +<% end -%> +<% if @slave -%> +slave = <%= @slave %> +<% end -%> +<% if @source -%> +source = <%= @source %> +<% end -%> +<% if @only -%> +# Slave only: specify a single database to replicate +only = <%= @only %> +<% end -%> +<% if @master -%> +master = <%= @master %> +<% end -%> +<% if @directoryperdb -%> +# Alters the storage pattern of the data directory to store each database +# files in a distinct folder. +directoryperdb = <%= @directoryperdb %> +<% end -%> +<% if @replset -%> +# Configure ReplicaSet membership +replSet = <%= @replset %> +<% end -%> +<% if @smallfiles -%> +# Use a smaller default data file size. +smallfiles = <%= @smallfiles %> +<% end -%> +<% if @rest -%> +# Enable rest API (disabled by default) +rest = <%= @rest %> +<% end -%> +<% if @profile -%> +# Modify this value to changes the level of database profiling, which inserts +# information about operation performance into output of mongod or the log file. +#0 = Off. No profiling. default +#1 = On. Only includes slow operations. +#2 = On. Includes all operations. +profile = <%= @profile %> +<% end -%> +<% if @slowms -%> +# Sets the threshold in milliseconds for mongod to consider a query slow for the profiler. +slowms = <%= @slowms %> +<% end -%> +<% if @keyfile -%> +# Specify the path to a key file to store authentication information. +keyFile = <%= @keyfile %> +<% end -%> +<% if @set_parameter -%> +setParameter = <%= @set_parameter %> +<% end -%> +<% if @syslog -%> +syslog = <%= @syslog %> +<% end -%> diff --git a/mongodb/tests/globals.pp b/mongodb/tests/globals.pp new file mode 100644 index 000000000..8166214bb --- /dev/null +++ b/mongodb/tests/globals.pp @@ -0,0 +1,3 @@ +class { 'mongodb::globals': + manage_package_repo => true +} diff --git a/mongodb/tests/init.pp b/mongodb/tests/init.pp new file mode 100644 index 000000000..aac044ee0 --- /dev/null +++ b/mongodb/tests/init.pp @@ -0,0 +1 @@ +class { '::mongodb': } diff --git a/mongodb/tests/replicaset.pp b/mongodb/tests/replicaset.pp new file mode 100644 index 000000000..a758b8dd2 --- /dev/null +++ b/mongodb/tests/replicaset.pp @@ -0,0 +1,16 @@ +node default { + class { '::mongodb::globals': + manage_package_repo => true + } -> + class { '::mongodb::server': + smallfiles => true, + bind_ip => ['0.0.0.0'], + replset => 'rsmain' + } +} + +node /mongo1/ inherits default { + mongodb_replset{'rsmain': + members => ['mongo1:27017', 'mongo2:27017', 'mongo3:27017'] + } +} diff --git a/mongodb/tests/server.pp b/mongodb/tests/server.pp new file mode 100644 index 000000000..95106ebc9 --- /dev/null +++ b/mongodb/tests/server.pp @@ -0,0 +1,2 @@ +class { 'mongodb::globals': manage_package_repo => true }-> +class { 'mongodb::server': } diff --git a/mysql b/mysql deleted file mode 160000 index c70fc13fc..000000000 --- a/mysql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c70fc13fc15740b61b8eccd3c79168d3e417a374 diff --git a/mysql/.fixtures.yml b/mysql/.fixtures.yml new file mode 100644 index 000000000..5631e2a23 --- /dev/null +++ b/mysql/.fixtures.yml @@ -0,0 +1,5 @@ +fixtures: + repositories: + "stdlib": "https://github.com/puppetlabs/puppetlabs-stdlib" + symlinks: + "mysql": "#{source_dir}" diff --git a/mysql/.gitignore b/mysql/.gitignore new file mode 100644 index 000000000..b5b7a00d6 --- /dev/null +++ b/mysql/.gitignore @@ -0,0 +1,7 @@ +pkg/ +Gemfile.lock +vendor/ +spec/fixtures/ +.vagrant/ +.bundle/ +coverage/ diff --git a/mysql/.nodeset.yml b/mysql/.nodeset.yml new file mode 100644 index 000000000..767f9cd2f --- /dev/null +++ b/mysql/.nodeset.yml @@ -0,0 +1,31 @@ +--- +default_set: 'centos-64-x64' +sets: + 'centos-59-x64': + nodes: + "main.foo.vm": + prefab: 'centos-59-x64' + 'centos-64-x64': + nodes: + "main.foo.vm": + prefab: 'centos-64-x64' + 'fedora-18-x64': + nodes: + "main.foo.vm": + prefab: 'fedora-18-x64' + 'debian-607-x64': + nodes: + "main.foo.vm": + prefab: 'debian-607-x64' + 'debian-70rc1-x64': + nodes: + "main.foo.vm": + prefab: 'debian-70rc1-x64' + 'ubuntu-server-10044-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-10044-x64' + 'ubuntu-server-12042-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-12042-x64' diff --git a/mysql/.sync.yml b/mysql/.sync.yml new file mode 100644 index 000000000..d35f21205 --- /dev/null +++ b/mysql/.sync.yml @@ -0,0 +1,7 @@ +--- +Gemfile: + optional: + - gem: mime-types + version: '<2.0' +spec/spec_helper.rb: + unmanaged: true diff --git a/mysql/.travis.yml b/mysql/.travis.yml new file mode 100644 index 000000000..a40ae502e --- /dev/null +++ b/mysql/.travis.yml @@ -0,0 +1,17 @@ +--- +language: ruby +bundler_args: --without development +script: "bundle exec rake validate && bundle exec rake lint && bundle exec rake spec SPEC_OPTS='--format documentation'" +matrix: + fast_finish: true + include: + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.6.0" + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.7.0" + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 3.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 3.0" +notifications: + email: false diff --git a/mysql/CHANGELOG.md b/mysql/CHANGELOG.md new file mode 100644 index 000000000..5f004f98b --- /dev/null +++ b/mysql/CHANGELOG.md @@ -0,0 +1,503 @@ +##2014-07-15 - Supported Release 2.3.1 +###Summary + +This release merely updates metadata.json so the module can be uninstalled and +upgraded via the puppet module command. + +##2014-05-14 - Supported Release 2.3.0 + +This release primarily adds support for RHEL7 and Ubuntu 14.04 but it +also adds a couple of new parameters to allow for further customization, +as well as ensuring backups can backup stored procedures properly. + +####Features +Added `execpath` to allow a custom executable path for non-standard mysql installations. +Added `dbname` to mysql::db and use ensure_resource to create the resource. +Added support for RHEL7 and Fedora Rawhide. +Added support for Ubuntu 14.04. +Create a warning for if you disable SSL. +Ensure the error logfile is owned by MySQL. +Disable ssl on FreeBSD. +Add PROCESS privilege for backups. + +####Bugfixes + +####Known Bugs +* No known bugs + +##2014-03-04 - Supported Release 2.2.3 +###Summary + +This is a supported release. This release removes a testing symlink that can +cause trouble on systems where /var is on a seperate filesystem from the +modulepath. + +####Features +####Bugfixes +####Known Bugs +* No known bugs + +##2014-03-04 - Supported Release 2.2.2 +###Summary +This is a supported release. Mostly comprised of enhanced testing, plus a +bugfix for Suse. + +####Bugfixes +- PHP bindings on Suse +- Test fixes + +####Known Bugs +* No known bugs + +##2014-02-19 - Version 2.2.1 + +###Summary + +Minor release that repairs mysql_database{} so that it sees the correct +collation settings (it was only checking the global mysql ones, not the +actual database and constantly setting it over and over since January 22nd). + +Also fixes a bunch of tests on various platforms. + + +##2014-02-13 - Version 2.2.0 + +###Summary + +####Features +- Add `backupdirmode`, `backupdirowner`, `backupdirgroup` to + mysql::server::backup to allow customizing the mysqlbackupdir. +- Support multiple options of the same name, allowing you to + do 'replicate-do-db' => ['base1', 'base2', 'base3'] in order to get three + lines of replicate-do-db = base1, replicate-do-db = base2 etc. + +####Bugfixes +- Fix `restart` so it actually stops mysql restarting if set to false. +- DRY out the defaults_file functionality in the providers. +- mysql_grant fixed to work with root@localhost/@. +- mysql_grant fixed for WITH MAX_QUERIES_PER_HOUR +- mysql_grant fixed so revoking all privileges accounts for GRANT OPTION +- mysql_grant fixed to remove duplicate privileges. +- mysql_grant fixed to handle PROCEDURES when removing privileges. +- mysql_database won't try to create existing databases, breaking replication. +- bind_address renamed bind-address in 'mysqld' options. +- key_buffer renamed to key_buffer_size. +- log_error renamed to log-error. +- pid_file renamed to pid-file. +- Ensure mysql::server:root_password runs before mysql::server::backup +- Fix options_override -> override_options in the README. +- Extensively rewrite the README to be accurate and awesome. +- Move to requiring stdlib 3.2.0, shipped in PE3.0 +- Add many new tests. + + +##2013-11-13 - Version 2.1.0 + +###Summary + +The most important changes in 2.1.0 are improvements to the my.cnf creation, +as well as providers. Setting options to = true strips them to be just the +key name itself, which is required for some options. + +The provider updates fix a number of bugs, from lowercase privileges to +deprecation warnings. + +Last, the new hiera integration functionality should make it easier to +externalize all your grants, users, and, databases. Another great set of +community submissions helped to make this release. + +####Features +- Some options can not take a argument. Gets rid of the '= true' when an +option is set to true. +- Easier hiera integration: Add hash parameters to mysql::server to allow +specifying grants, users, and databases. + +####Bugfixes +- Fix an issue with lowercase privileges in mysql_grant{} causing them to be reapplied needlessly. +- Changed defaults-file to defaults-extra-file in providers. +- Ensure /root/.my.cnf is 0600 and root owned. +- database_user deprecation warning was incorrect. +- Add anchor pattern for client.pp +- Documentation improvements. +- Various test fixes. + + +##2013-10-21 - Version 2.0.1 + +###Summary + +This is a bugfix release to handle an issue where unsorted mysql_grant{} +privileges could cause Puppet to incorrectly reapply the permissions on +each run. + +####Bugfixes +- Mysql_grant now sorts privileges in the type and provider for comparison. +- Comment and test tweak for PE3.1. + + +##2013-10-14 - Version 2.0.0 + +###Summary + +(Previously detailed in the changelog for 2.0.0-rc1) + +This module has been completely refactored and works significantly different. +The changes are broad and touch almost every piece of the module. + +See the README.md for full details of all changes and syntax. +Please remain on 1.0.0 if you don't have time to fully test this in dev. + +* mysql::server, mysql::client, and mysql::bindings are the primary interface +classes. +* mysql::server takes an `override_options` parameter to set my.cnf options, +with the hash format: { 'section' => { 'thing' => 'value' }} +* mysql attempts backwards compatibility by forwarding all parameters to +mysql::server. + + +##2013-10-09 - Version 2.0.0-rc5 + +###Summary + +Hopefully the final rc! Further fixes to mysql_grant (stripping out the +cleverness so we match a much wider range of input.) + +####Bugfixes +- Make mysql_grant accept '.*'@'.*' in terms of input for user@host. + + +##2013-10-09 - Version 2.0.0-rc4 + +###Summary + +Bugfixes to mysql_grant and mysql_user form the bulk of this rc, as well as +ensuring that values in the override_options hash that contain a value of '' +are created as just "key" in the conf rather than "key =" or "key = false". + +####Bugfixes +- Improve mysql_grant to work with IPv6 addresses (both long and short). +- Ensure @host users work as well as user@host users. +- Updated my.cnf template to support items with no values. + + +##2013-10-07 - Version 2.0.0-rc3 + +###Summary +Fix mysql::server::monitor's use of mysql_user{}. + +####Bugfixes +- Fix myql::server::monitor's use of mysql_user{} to grant the proper +permissions. Add specs as well. (Thanks to treydock!) + + +##2013-10-03 - Version 2.0.0-rc2 + +###Summary +Bugfixes + +####Bugfixes +- Fix a duplicate parameter in mysql::server + + +##2013-10-03 - Version 2.0.0-rc1 + +###Summary + +This module has been completely refactored and works significantly different. +The changes are broad and touch almost every piece of the module. + +See the README.md for full details of all changes and syntax. +Please remain on 1.0.0 if you don't have time to fully test this in dev. + +* mysql::server, mysql::client, and mysql::bindings are the primary interface +classes. +* mysql::server takes an `override_options` parameter to set my.cnf options, +with the hash format: { 'section' => { 'thing' => 'value' }} +* mysql attempts backwards compatibility by forwarding all parameters to +mysql::server. + +--- +##2013-09-23 - Version 1.0.0 + +###Summary + +This release introduces a number of new type/providers, to eventually +replace the database_ ones. The module has been converted to call the +new providers rather than the previous ones as they have a number of +fixes, additional options, and work with puppet resource. + +This 1.0.0 release precedes a large refactoring that will be released +almost immediately after as 2.0.0. + +####Features +- Added mysql_grant, mysql_database, and mysql_user. +- Add `mysql::bindings` class and refactor all other bindings to be contained underneath mysql::bindings:: namespace. +- Added support to back up specified databases only with 'mysqlbackup' parameter. +- Add option to mysql::backup to set the backup script to perform a mysqldump on each database to its own file + +####Bugfixes +- Update my.cnf.pass.erb to allow custom socket support +- Add environment variable for .my.cnf in mysql::db. +- Add HOME environment variable for .my.cnf to mysqladmin command when +(re)setting root password + +--- +##2013-07-15 - Version 0.9.0 +####Features +- Add `mysql::backup::backuprotate` parameter +- Add `mysql::backup::delete_before_dump` parameter +- Add `max_user_connections` attribute to `database_user` type + +####Bugfixes +- Add client package dependency for `mysql::db` +- Remove duplicate `expire_logs_days` and `max_binlog_size` settings +- Make root's `.my.cnf` file path dynamic +- Update pidfile path for Suse variants +- Fixes for lint + +##2013-07-05 - Version 0.8.1 +####Bugfixes + - Fix a typo in the Fedora 19 support. + +##2013-07-01 - Version 0.8.0 +####Features + - mysql::perl class to install perl-DBD-mysql. + - minor improvements to the providers to improve reliability + - Install the MariaDB packages on Fedora 19 instead of MySQL. + - Add new `mysql` class parameters: + - `max_connections`: The maximum number of allowed connections. + - `manage_config_file`: Opt out of puppetized control of my.cnf. + - `ft_min_word_len`: Fine tune the full text search. + - `ft_max_word_len`: Fine tune the full text search. + - Add new `mysql` class performance tuning parameters: + - `key_buffer` + - `thread_stack` + - `thread_cache_size` + - `myisam-recover` + - `query_cache_limit` + - `query_cache_size` + - `max_connections` + - `tmp_table_size` + - `table_open_cache` + - `long_query_time` + - Add new `mysql` class replication parameters: + - `server_id` + - `sql_log_bin` + - `log_bin` + - `max_binlog_size` + - `binlog_do_db` + - `expire_logs_days` + - `log_bin_trust_function_creators` + - `replicate_ignore_table` + - `replicate_wild_do_table` + - `replicate_wild_ignore_table` + - `expire_logs_days` + - `max_binlog_size` + +####Bugfixes + - No longer restart MySQL when /root/.my.cnf changes. + - Ensure mysql::config runs before any mysql::db defines. + +##2013-06-26 - Version 0.7.1 +####Bugfixes +- Single-quote password for special characters +- Update travis testing for puppet 3.2.x and missing Bundler gems + +##2013-06-25 - Version 0.7.0 +This is a maintenance release for community bugfixes and exposing +configuration variables. + +* Add new `mysql` class parameters: + - `basedir`: The base directory mysql uses + - `bind_address`: The IP mysql binds to + - `client_package_name`: The name of the mysql client package + - `config_file`: The location of the server config file + - `config_template`: The template to use to generate my.cnf + - `datadir`: The directory MySQL's datafiles are stored + - `default_engine`: The default engine to use for tables + - `etc_root_password`: Whether or not to add the mysql root password to + /etc/my.cnf + - `java_package_name`: The name of the java package containing the java + connector + - `log_error`: Where to log errors + - `manage_service`: Boolean dictating if mysql::server should manage the + service + - `max_allowed_packet`: Maximum network packet size mysqld will accept + - `old_root_password`: Previous root user password + - `php_package_name`: The name of the phpmysql package to install + - `pidfile`: The location mysql will expect the pidfile to be + - `port`: The port mysql listens on + - `purge_conf_dir`: Value fed to recurse and purge parameters of the + /etc/mysql/conf.d resource + - `python_package_name`: The name of the python mysql package to install + - `restart`: Whether to restart mysqld + - `root_group`: Use specified group for root-owned files + - `root_password`: The root MySQL password to use + - `ruby_package_name`: The name of the ruby mysql package to install + - `ruby_package_provider`: The installation suite to use when installing the + ruby package + - `server_package_name`: The name of the server package to install + - `service_name`: The name of the service to start + - `service_provider`: The name of the service provider + - `socket`: The location of the MySQL server socket file + - `ssl_ca`: The location of the SSL CA Cert + - `ssl_cert`: The location of the SSL Certificate to use + - `ssl_key`: The SSL key to use + - `ssl`: Whether or not to enable ssl + - `tmpdir`: The directory MySQL's tmpfiles are stored +* Deprecate `mysql::package_name` parameter in favor of +`mysql::client_package_name` +* Fix local variable template deprecation +* Fix dependency ordering in `mysql::db` +* Fix ANSI quoting in queries +* Fix travis support (but still messy) +* Fix typos + +##2013-01-11 - Version 0.6.1 +* Fix providers when /root/.my.cnf is absent + +##2013-01-09 - Version 0.6.0 +* Add `mysql::server::config` define for specific config directives +* Add `mysql::php` class for php support +* Add `backupcompress` parameter to `mysql::backup` +* Add `restart` parameter to `mysql::config` +* Add `purge_conf_dir` parameter to `mysql::config` +* Add `manage_service` parameter to `mysql::server` +* Add syslog logging support via the `log_error` parameter +* Add initial SuSE support +* Fix remove non-localhost root user when fqdn != hostname +* Fix dependency in `mysql::server::monitor` +* Fix .my.cnf path for root user and root password +* Fix ipv6 support for users +* Fix / update various spec tests +* Fix typos +* Fix lint warnings + +##2012-08-23 - Version 0.5.0 +* Add puppetlabs/stdlib as requirement +* Add validation for mysql privs in provider +* Add `pidfile` parameter to mysql::config +* Add `ensure` parameter to mysql::db +* Add Amazon linux support +* Change `bind_address` parameter to be optional in my.cnf template +* Fix quoting root passwords + +##2012-07-24 - Version 0.4.0 +* Fix various bugs regarding database names +* FreeBSD support +* Allow specifying the storage engine +* Add a backup class +* Add a security class to purge default accounts + +##2012-05-03 - Version 0.3.0 +* 14218 Query the database for available privileges +* Add mysql::java class for java connector installation +* Use correct error log location on different distros +* Fix set_mysql_rootpw to properly depend on my.cnf + +##2012-04-11 - Version 0.2.0 + +##2012-03-19 - William Van Hevelingen +* (#13203) Add ssl support (f7e0ea5) + +##2012-03-18 - Nan Liu +* Travis ci before script needs success exit code. (0ea463b) + +##2012-03-18 - Nan Liu +* Fix Puppet 2.6 compilation issues. (9ebbbc4) + +##2012-03-16 - Nan Liu +* Add travis.ci for testing multiple puppet versions. (33c72ef) + +##2012-03-15 - William Van Hevelingen +* (#13163) Datadir should be configurable (f353fc6) + +##2012-03-16 - Nan Liu +* Document create_resources dependency. (558a59c) + +##2012-03-16 - Nan Liu +* Fix spec test issues related to error message. (eff79b5) + +##2012-03-16 - Nan Liu +* Fix mysql service on Ubuntu. (72da2c5) + +##2012-03-16 - Dan Bode +* Add more spec test coverage (55e399d) + +##2012-03-16 - Nan Liu +* (#11963) Fix spec test due to path changes. (1700349) + +##2012-03-07 - François Charlier +* Add a test to check path for 'mysqld-restart' (b14c7d1) + +##2012-03-07 - François Charlier +* Fix path for 'mysqld-restart' (1a9ae6b) + +##2012-03-15 - Dan Bode +* Add rspec-puppet tests for mysql::config (907331a) + +##2012-03-15 - Dan Bode +* Moved class dependency between sever and config to server (da62ad6) + +##2012-03-14 - Dan Bode +* Notify mysql restart from set_mysql_rootpw exec (0832a2c) + +##2012-03-15 - Nan Liu +* Add documentation related to osfamily fact. (8265d28) + +##2012-03-14 - Dan Bode +* Mention osfamily value in failure message (e472d3b) + +##2012-03-14 - Dan Bode +* Fix bug when querying for all database users (015490c) + +##2012-02-09 - Nan Liu +* Major refactor of mysql module. (b1f90fd) + +##2012-01-11 - Justin Ellison +* Ruby and Python's MySQL libraries are named differently on different distros. (1e926b4) + +##2012-01-11 - Justin Ellison +* Per @ghoneycutt, we should fail explicitly and explain why. (09af083) + +##2012-01-11 - Justin Ellison +* Removing duplicate declaration (7513d03) + +##2012-01-10 - Justin Ellison +* Use socket value from params class instead of hardcoding. (663e97c) + +##2012-01-10 - Justin Ellison +* Instead of hardcoding the config file target, pull it from mysql::params (031a47d) + +##2012-01-10 - Justin Ellison +* Moved $socket to within the case to toggle between distros. Added a $config_file variable to allow per-distro config file destinations. (360eacd) + +##2012-01-10 - Justin Ellison +* Pretty sure this is a bug, 99% of Linux distros out there won't ever hit the default. (3462e6b) + +##2012-02-09 - William Van Hevelingen +* Changed the README to use markdown (3b7dfeb) + +##2012-02-04 - Daniel Black +* (#12412) mysqltuner.pl update (b809e6f) + +##2011-11-17 - Matthias Pigulla +* (#11363) Add two missing privileges to grant: event_priv, trigger_priv (d15c9d1) + +##2011-12-20 - Jeff McCune +* (minor) Fixup typos in Modulefile metadata (a0ed6a1) + +##2011-12-19 - Carl Caum +* Only notify Exec to import sql if sql is given (0783c74) + +##2011-12-19 - Carl Caum +* (#11508) Only load sql_scripts on DB creation (e3b9fd9) + +##2011-12-13 - Justin Ellison +* Require not needed due to implicit dependencies (3058feb) + +##2011-12-13 - Justin Ellison +* Bug #11375: puppetlabs-mysql fails on CentOS/RHEL (a557b8d) + +##2011-06-03 - Dan Bode - 0.0.1 +* initial commit diff --git a/mysql/CONTRIBUTING.md b/mysql/CONTRIBUTING.md new file mode 100644 index 000000000..e1288478a --- /dev/null +++ b/mysql/CONTRIBUTING.md @@ -0,0 +1,234 @@ +Checklist (and a short version for the impatient) +================================================= + + * Commits: + + - Make commits of logical units. + + - Check for unnecessary whitespace with "git diff --check" before + committing. + + - Commit using Unix line endings (check the settings around "crlf" in + git-config(1)). + + - Do not check in commented out code or unneeded files. + + - The first line of the commit message should be a short + description (50 characters is the soft limit, excluding ticket + number(s)), and should skip the full stop. + + - Associate the issue in the message. The first line should include + the issue number in the form "(#XXXX) Rest of message". + + - The body should provide a meaningful commit message, which: + + - uses the imperative, present tense: "change", not "changed" or + "changes". + + - includes motivation for the change, and contrasts its + implementation with the previous behavior. + + - Make sure that you have tests for the bug you are fixing, or + feature you are adding. + + - Make sure the test suites passes after your commit: + `bundle exec rspec spec/acceptance` More information on [testing](#Testing) below + + - When introducing a new feature, make sure it is properly + documented in the README.md + + * Submission: + + * Pre-requisites: + + - Sign the [Contributor License Agreement](https://cla.puppetlabs.com/) + + - Make sure you have a [GitHub account](https://github.com/join) + + - [Create a ticket](http://projects.puppetlabs.com/projects/modules/issues/new), or [watch the ticket](http://projects.puppetlabs.com/projects/modules/issues) you are patching for. + + * Preferred method: + + - Fork the repository on GitHub. + + - Push your changes to a topic branch in your fork of the + repository. (the format ticket/1234-short_description_of_change is + usually preferred for this project). + + - Submit a pull request to the repository in the puppetlabs + organization. + +The long version +================ + + 1. Make separate commits for logically separate changes. + + Please break your commits down into logically consistent units + which include new or changed tests relevant to the rest of the + change. The goal of doing this is to make the diff easier to + read for whoever is reviewing your code. In general, the easier + your diff is to read, the more likely someone will be happy to + review it and get it into the code base. + + If you are going to refactor a piece of code, please do so as a + separate commit from your feature or bug fix changes. + + We also really appreciate changes that include tests to make + sure the bug is not re-introduced, and that the feature is not + accidentally broken. + + Describe the technical detail of the change(s). If your + description starts to get too long, that is a good sign that you + probably need to split up your commit into more finely grained + pieces. + + Commits which plainly describe the things which help + reviewers check the patch and future developers understand the + code are much more likely to be merged in with a minimum of + bike-shedding or requested changes. Ideally, the commit message + would include information, and be in a form suitable for + inclusion in the release notes for the version of Puppet that + includes them. + + Please also check that you are not introducing any trailing + whitespace or other "whitespace errors". You can do this by + running "git diff --check" on your changes before you commit. + + 2. Sign the Contributor License Agreement + + Before we can accept your changes, we do need a signed Puppet + Labs Contributor License Agreement (CLA). + + You can access the CLA via the [Contributor License Agreement link](https://cla.puppetlabs.com/) + + If you have any questions about the CLA, please feel free to + contact Puppet Labs via email at cla-submissions@puppetlabs.com. + + 3. Sending your patches + + To submit your changes via a GitHub pull request, we _highly_ + recommend that you have them on a topic branch, instead of + directly on "master". + It makes things much easier to keep track of, especially if + you decide to work on another thing before your first change + is merged in. + + GitHub has some pretty good + [general documentation](http://help.github.com/) on using + their site. They also have documentation on + [creating pull requests](http://help.github.com/send-pull-requests/). + + In general, after pushing your topic branch up to your + repository on GitHub, you can switch to the branch in the + GitHub UI and click "Pull Request" towards the top of the page + in order to open a pull request. + + + 4. Update the related GitHub issue. + + If there is a GitHub issue associated with the change you + submitted, then you should update the ticket to include the + location of your branch, along with any other commentary you + may wish to make. + +Testing +======= + +Getting Started +--------------- + +Our puppet modules provide [`Gemfile`](./Gemfile)s which can tell a ruby +package manager such as [bundler](http://bundler.io/) what Ruby packages, +or Gems, are required to build, develop, and test this software. + +Please make sure you have [bundler installed](http://bundler.io/#getting-started) +on your system, then use it to install all dependencies needed for this project, +by running + +```shell +% bundle install +Fetching gem metadata from https://rubygems.org/........ +Fetching gem metadata from https://rubygems.org/.. +Using rake (10.1.0) +Using builder (3.2.2) +-- 8><-- many more --><8 -- +Using rspec-system-puppet (2.2.0) +Using serverspec (0.6.3) +Using rspec-system-serverspec (1.0.0) +Using bundler (1.3.5) +Your bundle is complete! +Use `bundle show [gemname]` to see where a bundled gem is installed. +``` + +NOTE some systems may require you to run this command with sudo. + +If you already have those gems installed, make sure they are up-to-date: + +```shell +% bundle update +``` + +With all dependencies in place and up-to-date we can now run the tests: + +```shell +% rake spec +``` + +This will execute all the [rspec tests](http://rspec-puppet.com/) tests +under [spec/defines](./spec/defines), [spec/classes](./spec/classes), +and so on. rspec tests may have the same kind of dependencies as the +module they are testing. While the module defines in its [Modulefile](./Modulefile), +rspec tests define them in [.fixtures.yml](./fixtures.yml). + +Some puppet modules also come with [beaker](https://github.com/puppetlabs/beaker) +tests. These tests spin up a virtual machine under +[VirtualBox](https://www.virtualbox.org/)) with, controlling it with +[Vagrant](http://www.vagrantup.com/) to actually simulate scripted test +scenarios. In order to run these, you will need both of those tools +installed on your system. + +You can run them by issuing the following command + +```shell +% rake spec_clean +% rspec spec/acceptance +``` + +This will now download a pre-fabricated image configured in the [default node-set](./spec/acceptance/nodesets/default.yml), +install puppet, copy this module and install its dependencies per [spec/spec_helper_acceptance.rb](./spec/spec_helper_acceptance.rb) +and then run all the tests under [spec/acceptance](./spec/acceptance). + +Writing Tests +------------- + +XXX getting started writing tests. + +If you have commit access to the repository +=========================================== + +Even if you have commit access to the repository, you will still need to +go through the process above, and have someone else review and merge +in your changes. The rule is that all changes must be reviewed by a +developer on the project (that did not write the code) to ensure that +all changes go through a code review process. + +Having someone other than the author of the topic branch recorded as +performing the merge is the record that they performed the code +review. + + +Additional Resources +==================== + +* [Getting additional help](http://projects.puppetlabs.com/projects/puppet/wiki/Getting_Help) + +* [Writing tests](http://projects.puppetlabs.com/projects/puppet/wiki/Development_Writing_Tests) + +* [Patchwork](https://patchwork.puppetlabs.com) + +* [Contributor License Agreement](https://projects.puppetlabs.com/contributor_licenses/sign) + +* [General GitHub documentation](http://help.github.com/) + +* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) + diff --git a/mysql/Gemfile b/mysql/Gemfile new file mode 100644 index 000000000..31c883e91 --- /dev/null +++ b/mysql/Gemfile @@ -0,0 +1,25 @@ +source ENV['GEM_SOURCE'] || "https://rubygems.org" + +group :development, :test do + gem 'rake', :require => false + gem 'rspec-puppet', :require => false + gem 'puppetlabs_spec_helper', :require => false + gem 'serverspec', :require => false + gem 'puppet-lint', :require => false + gem 'beaker-rspec', :require => false + gem 'puppet_facts', :require => false +end + +if facterversion = ENV['FACTER_GEM_VERSION'] + gem 'facter', facterversion, :require => false +else + gem 'facter', :require => false +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +# vim:ft=ruby diff --git a/mysql/LICENSE b/mysql/LICENSE new file mode 100644 index 000000000..297f85cfa --- /dev/null +++ b/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 2013 Puppet Labs + + 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/mysql/README.md b/mysql/README.md new file mode 100644 index 000000000..379d88a05 --- /dev/null +++ b/mysql/README.md @@ -0,0 +1,550 @@ +#MySQL + +####Table of Contents + +1. [Overview](#overview) +2. [Module Description - What the module does and why it is useful](#module-description) +3. [Backwards compatibility information](#backwards-compatibility) +3. [Setup - The basics of getting started with mysql](#setup) + * [What mysql affects](#what-mysql-affects) + * [Beginning with mysql](#beginning-with-mysql) +4. [Usage - Configuration options and additional functionality](#usage) +5. [Reference - An under-the-hood peek at what the module is doing and how](#reference) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) + +##Overview + +The mysql module installs, configures, and manages the MySQL service. + +##Module Description + +The mysql module manages both the installation and configuration of MySQL as +well as extends Puppet to allow management of MySQL resources, such as +databases, users, and grants. + +##Backwards Compatibility + +This module has just undergone a very large rewrite. Some new classes have been added, and many previous classes and configurations work differently than before. We've attempted to handle backwards compatibility automatically by adding a +`attempt_compatibility_mode` parameter to the main mysql class. If you set +this to 'true' it will attempt to map your previous parameters into the new +`mysql::server` class. + +#####WARNING + +Compatibility mode may fail. It may eat your MySQL server. PLEASE test it before running it live, even if the test is just a no-op and manual comparison. Please be careful! + +##Setup + +###What MySQL affects + +* MySQL package +* MySQL configuration files +* MySQL service + +###Beginning with MySQL + +If you just want a server installed with the default options you can run +`include '::mysql::server'`. + +If you need to customize options, such as the root +password or `/etc/my.cnf` settings, then you must also pass in an override hash: + +```puppet +class { '::mysql::server': + root_password => 'strongpassword', + override_options => $override_options +} +``` +(see 'Overrides' below for examples of the hash structure for `$override_options`) + +##Usage + +All interaction for the server is done via `mysql::server`. To install the +client you use `mysql::client`, and to install bindings you can use +`mysql::bindings`. + +###Overrides + +The hash structure for overrides in `mysql::server` is as follows: + +```puppet +$override_options = { + 'section' => { + 'item' => 'thing', + } +} +``` + +For items that you would traditionally represent as: + +
+[section]
+thing = X
+
+ +You can just make an entry like `thing => true`, `thing => value`, or `thing => "` in the hash. You can also pass an array `thing => ['value', 'value2']` or even list each `thing => value` separately on separate lines. MySQL doesn't care if 'thing' is alone or set to a value; it'll happily accept both. To keep an option out of the my.cnf file, e.g. when using override_options to revert to a default value, you can pass thing => undef. +If an option needs multiple instances, you can pass an array. For example + +```puppet +$override_options = { + 'mysqld' => { + 'replicate-do-db' => ['base1', 'base2'], + } +} +``` + +will produce + +
+[mysql]
+replicate-do-db = base1
+replicate-do-db = base2
+
+ +###Custom configuration + +To add custom MySQL configuration, drop additional files into +`includedir`. Dropping files into `includedir` allows you to override settings or add additional ones, which is helpful if you choose not to use `override_options` in `mysql::server`. The `includedir` location is by default set to /etc/mysql/conf.d. + +##Reference + +###Classes + +####Public classes +* `mysql::server`: Installs and configures MySQL. +* `mysql::server::account_security`: Deletes default MySQL accounts. +* `mysql::server::monitor`: Sets up a monitoring user. +* `mysql::server::mysqltuner`: Installs MySQL tuner script. +* `mysql::server::backup`: Sets up MySQL backups via cron. +* `mysql::bindings`: Installs various MySQL language bindings. +* `mysql::client`: Installs MySQL client (for non-servers). + +####Private classes +* `mysql::server::install`: Installs packages. +* `mysql::server::config`: Configures MYSQL. +* `mysql::server::service`: Manages service. +* `mysql::server::root_password`: Sets MySQL root password. +* `mysql::server::providers`: Creates users, grants, and databases. +* `mysql::bindings::java`: Installs Java bindings. +* `mysql::bindings::perl`: Installs Perl bindings. +* `mysql::bindings::python`: Installs Python bindings. +* `mysql::bindings::ruby`: Installs Ruby bindings. +* `mysql::client::install`: Installs MySQL client. + +###Parameters + +####mysql::server + +#####`root_password` + +The MySQL root password. Puppet will attempt to set the root password and update `/root/.my.cnf` with it. + +#####`old_root_password` + +The previous root password (**REQUIRED** if you wish to change the root password via Puppet.) + +#####`override_options` + +The hash of override options to pass into MySQL. It can be structured +like a hash in the my.cnf file, so entries look like + +```puppet +$override_options = { + 'section' => { + 'item' => 'thing', + } +} +``` + +For items that you would traditionally represent as: + +
+[section]
+thing = X
+
+ +You can just make an entry like `thing => true`, `thing => value`, or `thing => "` in the hash. You can also pass an array `thing => ['value', 'value2']` or even list each `thing => value` separately on separate lines. MySQL doesn't care if 'thing' is alone or set to a value; it'll happily accept both. To keep an option out of the my.cnf file, e.g. when using override_options to revert to a default value, you can pass thing => undef. + +#####`config_file` + +The location of the MySQL configuration file. + +#####`manage_config_file` + +Whether the MySQL configuration file should be managed. + +#####`includedir` +The location of !includedir for custom configuration overrides. + +#####`purge_conf_dir` + +Whether the `includedir` directory should be purged. + +#####`restart` + +Whether the service should be restarted when things change. + +#####`root_group` + +What is the group used for root? + +#####`package_ensure` + +What to set the package to. Can be 'present', 'absent', or 'x.y.z'. + +#####`package_name` + +The name of the mysql server package to install. + +#####`remove_default_accounts` + +Boolean to decide if we should automatically include +`mysql::server::account_security`. + +#####`service_enabled` + +Boolean to decide if the service should be enabled. + +#####`service_manage` + +Boolean to decide if the service should be managed. + +#####`service_name` + +The name of the mysql server service. + +#####`service_provider` + +The provider to use to manage the service. + +#####`users` + +Optional hash of users to create, which are passed to [mysql_user](#mysql_user). + +``` +users => { + 'someuser@localhost' => { + ensure => 'present', + max_connections_per_hour => '0', + max_queries_per_hour => '0', + max_updates_per_hour => '0', + max_user_connections => '0', + password_hash => '*F3A2A51A9B0F2BE2468926B4132313728C250DBF', + }, +} +``` + +#####`grants` + +Optional hash of grants, which are passed to [mysql_grant](#mysql_grant). + +``` +grants => { + 'someuser@localhost/somedb.*' => { + ensure => 'present', + options => ['GRANT'], + privileges => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'], + table => 'somedb.*', + user => 'someuser@localhost', + }, +} +``` + +#####`databases` + +Optional hash of databases to create, which are passed to [mysql_database](#mysql_database). + +``` +databases => { + 'somedb' => { + ensure => 'present', + charset => 'utf8', + }, +} +``` + +####mysql::server::backup + +#####`backupuser` + +MySQL user to create for backups. + +#####`backuppassword` + +MySQL user password for backups. + +#####`backupdir` + +Directory to back up into. + +#####`backupdirmode` + +Permissions applied to the backup directory. This parameter is passed directly +to the `file` resource. + +#####`backupdirowner` + +Owner for the backup directory. This parameter is passed directly to the `file` +resource. + +#####`backupdirgroup` + +Group owner for the backup directory. This parameter is passed directly to the +`file` resource. + +#####`backupcompress` + +Boolean to determine if backups should be compressed. + +#####`backuprotate` + +How many days to keep backups for. + +#####`delete_before_dump` + +Boolean to determine if you should cleanup before backing up or after. + +#####`backupdatabases` + +Array of databases to specifically back up. + +#####`file_per_database` + +Whether a separate file be used per database. + +#####`ensure` + +Allows you to remove the backup scripts. Can be 'present' or 'absent'. + +#####`execpath` + +Allows you to set a custom PATH should your mysql installation be non-standard places. Defaults to `/usr/bin:/usr/sbin:/bin:/sbin` + +#####`time` + +An array of two elements to set the backup time. Allows ['23', '5'] or ['3', '45'] for HH:MM times. + +#####`postscript` + +A script that is executed at when the backup is finished. This could be used to (r)sync the backup to a central store. This script can be either a single line that is directly executed or a number of lines, when supplied as an array. It could also be one or more externally managed (executable) files. + +####mysql::server::monitor + +#####`mysql_monitor_username` + +The username to create for MySQL monitoring. + +#####`mysql_monitor_password` + +The password to create for MySQL monitoring. + +#####`mysql_monitor_hostname` + +The hostname to allow to access the MySQL monitoring user. + +####mysql::bindings + +#####`java_enable` + +Boolean to decide if the Java bindings should be installed. + +#####`perl_enable` + +Boolean to decide if the Perl bindings should be installed. + +#####`php_enable` + +Boolean to decide if the PHP bindings should be installed. + +#####`python_enable` + +Boolean to decide if the Python bindings should be installed. + +#####`ruby_enable` + +Boolean to decide if the Ruby bindings should be installed. + +#####`java_package_ensure` + +What to set the package to. Can be 'present', 'absent', or 'x.y.z'. + +#####`java_package_name` + +The name of the package to install. + +#####`java_package_provider` + +What provider should be used to install the package. + +#####`perl_package_ensure` + +What to set the package to. Can be 'present', 'absent', or 'x.y.z'. + +#####`perl_package_name` + +The name of the package to install. + +#####`perl_package_provider` + +What provider should be used to install the package. + +#####`python_package_ensure` + +What to set the package to. Can be 'present', 'absent', or 'x.y.z'. + +#####`python_package_name` + +The name of the package to install. + +#####`python_package_provider` + +What provider should be used to install the package. + +#####`ruby_package_ensure` + +What to set the package to. Can be 'present', 'absent', or 'x.y.z'. + +#####`ruby_package_name` + +The name of the package to install. + +#####`ruby_package_provider` + +What provider should be used to install the package. + +####mysql::client + +#####`bindings_enable` + +Boolean to automatically install all bindings. + +#####`package_ensure` + +What to set the package to. Can be 'present', 'absent', or 'x.y.z'. + +#####`package_name` + +What is the name of the mysql client package to install. + +###Defines + +####mysql::db + +Creates a database with a user and assigns some privileges. + +```puppet + mysql::db { 'mydb': + user => 'myuser', + password => 'mypass', + host => 'localhost', + grant => ['SELECT', 'UPDATE'], + } +``` + +Or using a different resource name with exported resources, + +```puppet + @@mysql::db { "mydb_${fqdn}": + user => 'myuser', + password => 'mypass', + dbname => 'mydb', + host => ${fqdn}, + grant => ['SELECT', 'UPDATE'], + tag => $domain, + } +``` + +Then collect it on the remote DB server. + +```puppet + Mysql::Db <<| tag == $domain |>> +``` + +###Providers + +####mysql_database + +`mysql_database` can be used to create and manage databases within MySQL. + +```puppet +mysql_database { 'information_schema': + ensure => 'present', + charset => 'utf8', + collate => 'utf8_swedish_ci', +} +mysql_database { 'mysql': + ensure => 'present', + charset => 'latin1', + collate => 'latin1_swedish_ci', +} +``` + +####mysql_user + +`mysql_user` can be used to create and manage user grants within MySQL. + +```puppet +mysql_user { 'root@127.0.0.1': + ensure => 'present', + max_connections_per_hour => '0', + max_queries_per_hour => '0', + max_updates_per_hour => '0', + max_user_connections => '0', +} +``` + +####mysql_grant + +`mysql_grant` can be used to create grant permissions to access databases within +MySQL. To use it you must create the title of the resource as shown below, +following the pattern of `username@hostname/database.table`: + +```puppet +mysql_grant { 'root@localhost/*.*': + ensure => 'present', + options => ['GRANT'], + privileges => ['ALL'], + table => '*.*', + user => 'root@localhost', +} +``` + +##Limitations + +This module has been tested on: + +* RedHat Enterprise Linux 5/6 +* Debian 6/7 +* CentOS 5/6 +* Ubuntu 12.04 + +Testing on other platforms has been light and cannot be guaranteed. + +#Development + +Puppet Labs modules on the Puppet Forge are open projects, and community +contributions are essential for keeping them great. We can’t access the +huge number of platforms and myriad of hardware, software, and deployment +configurations that Puppet is intended to serve. + +We want to keep it as easy as possible to contribute changes so that our +modules work in your environment. There are a few guidelines that we need +contributors to follow so that we can have a chance of keeping on top of things. + +You can read the complete module contribution guide [on the Puppet Labs wiki.](http://projects.puppetlabs.com/projects/module-site/wiki/Module_contributing) + +### Authors + +This module is based on work by David Schmitt. The following contributors have contributed patches to this module (beyond Puppet Labs): + +* Larry Ludwig +* Christian G. Warden +* Daniel Black +* Justin Ellison +* Lowe Schmidt +* Matthias Pigulla +* William Van Hevelingen +* Michael Arnold +* Chris Weyl + diff --git a/mysql/Rakefile b/mysql/Rakefile new file mode 100644 index 000000000..5868545f2 --- /dev/null +++ b/mysql/Rakefile @@ -0,0 +1,10 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.fail_on_warnings +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_inherits_from_params_class') +PuppetLint.configuration.send('disable_class_parameter_defaults') +PuppetLint.configuration.send('disable_documentation') +PuppetLint.configuration.send('disable_single_quote_string_with_variables') +PuppetLint.configuration.ignore_paths = ["spec/**/*.pp", "pkg/**/*.pp"] diff --git a/mysql/TODO b/mysql/TODO new file mode 100644 index 000000000..391329393 --- /dev/null +++ b/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/mysql/files/mysqltuner.pl b/mysql/files/mysqltuner.pl new file mode 100644 index 000000000..ecf2cb16c --- /dev/null +++ b/mysql/files/mysqltuner.pl @@ -0,0 +1,1023 @@ +#!/usr/bin/perl -w +# mysqltuner.pl - Version 1.3.0 +# High Performance MySQL Tuning Script +# Copyright (C) 2006-2014 Major Hayden - major@mhtx.net +# +# For the latest updates, please visit http://mysqltuner.com/ +# Git repository available at http://github.com/major/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.3.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', + 'mysqladmin=s', + '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". + " --mysqladmin Path to a custom mysqladmin executable\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|FreeBSD/) { + $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` or memerror; + $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 $mysqladmincmd; + if ($opt{mysqladmin}) { + $mysqladmincmd = $opt{mysqladmin}; + } else { + $mysqladmincmd = `which mysqladmin`; + } + chomp($mysqladmincmd); + if (! -e $mysqladmincmd && $opt{mysqladmin}) { + badprint "Unable to find the mysqladmin command you specified: ".$mysqladmincmd."\n"; + exit; + } elsif (! -e $mysqladmincmd) { + badprint "Couldn't 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 = `$mysqladmincmd 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; + } + } + my $svcprop = `which svcprop 2>/dev/null`; + if (substr($svcprop, 0, 1) =~ "/") { + # We are on solaris + (my $mysql_login = `svcprop -p quickbackup/username svc:/network/mysql-quickbackup:default`) =~ s/\s+$//; + (my $mysql_pass = `svcprop -p quickbackup/password svc:/network/mysql-quickbackup:default`) =~ s/\s+$//; + if ( substr($mysql_login, 0, 7) ne "svcprop" ) { + # mysql-quickbackup is installed + $mysqllogin = "-u $mysql_login -p$mysql_pass"; + my $loginstatus = `mysqladmin $mysqllogin ping 2>&1`; + if ($loginstatus =~ /mysqld is alive/) { + goodprint "Logged in using credentials from mysql-quickbackup.\n"; + return 1; + } else { + badprint "Attempted to use login credentials from mysql-quickbackup, but they failed.\n"; + exit 0; + } + } + } elsif ( -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 = `$mysqladmincmd 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 "/usr/local/directadmin/conf/mysql.conf" and $doremote == 0 ){ + # It's a DirectAdmin box, use the available credentials + my $mysqluser=`cat /usr/local/directadmin/conf/mysql.conf | egrep '^user=.*'`; + my $mysqlpass=`cat /usr/local/directadmin/conf/mysql.conf | egrep '^passwd=.*'`; + + $mysqluser =~ s/user=//; + $mysqluser =~ s/[\r\n]//; + $mysqlpass =~ s/passwd=//; + $mysqlpass =~ s/[\r\n]//; + + $mysqllogin = "-u $mysqluser -p$mysqlpass"; + + my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`; + unless ($loginstatus =~ /mysqld is alive/) { + badprint "Attempted to use login credentials from DirectAdmin, 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 = `$mysqladmincmd $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 = `$mysqladmincmd $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 = `$mysqladmincmd 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 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"; + } elsif (`uname` =~ /NetBSD|OpenBSD/ && `sysctl -b hw.machine` =~ /64/) { + $arch = 64; + goodprint "Operating on 64-bit architecture\n"; + } elsif (`uname` =~ /FreeBSD/ && `sysctl -b hw.machine_arch` =~ /64/) { + $arch = 64; + goodprint "Operating on 64-bit architecture\n"; + } elsif (`uname` =~ /Darwin/ && `uname -m` =~ /Power Macintosh/) { + # Darwin box.local 9.8.0 Darwin Kernel Version 9.8.0: Wed Jul 15 16:57:01 PDT 2009; root:xnu1228.15.4~1/RELEASE_PPC Power Macintosh + $arch = 64; + goodprint "Operating on 64-bit architecture\n"; + } elsif (`uname` =~ /Darwin/ && `uname -m` =~ /x86_64/) { + # Darwin gibas.local 12.3.0 Darwin Kernel Version 12.3.0: Sun Jan 6 22:37:10 PST 2013; root:xnu-2050.22.13~1/RELEASE_X86_64 x86_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; + if (mysql_version_ge(5)) { + my @engineresults = `mysql $mysqllogin -Bse "SELECT ENGINE,SUPPORT FROM information_schema.ENGINES WHERE ENGINE NOT IN ('performance_schema','MyISAM','MERGE','MEMORY') ORDER BY ENGINE ASC"`; + foreach my $line (@engineresults) { + my ($engine,$engineenabled); + ($engine,$engineenabled) = $line =~ /([a-zA-Z_]*)\s+([a-zA-Z]+)/; + $engines .= ($engineenabled eq "YES" || $engineenabled eq "DEFAULT") ? greenwrap "+".$engine." " : redwrap "-".$engine." "; + } + } else { + $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) && $myvar{'key_buffer_size'} > 0) { + $mycalc{'pct_key_buffer_used'} = sprintf("%.1f",(1 - (($mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'}) / $myvar{'key_buffer_size'})) * 100); + } else { + $mycalc{'pct_key_buffer_used'} = 0; + } + 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 ($myvar{'query_cache_type'} eq "OFF") { + badprint "Query cache is disabled\n"; + push(@adjvars,"query_cache_type (=1)"); + } 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 + my $table_cache_var = ""; + 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)) { + $table_cache_var = "table_open_cache"; + } else { + $table_cache_var = "table_cache"; + } + push(@adjvars,$table_cache_var." (> ".$myvar{'table_open_cache'}.")"); + push(@generalrec,"Increase ".$table_cache_var." gradually to avoid file descriptor limits"); + push(@generalrec,"Read this before increasing ".$table_cache_var." over 64: http://bit.ly/1mi7c4C"); + } 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 buffer pool / data size: ".hr_bytes($myvar{'innodb_buffer_pool_size'})."/".hr_bytes($enginestats{'InnoDB'})."\n"; + } else { + badprint "InnoDB buffer pool / data size: ".hr_bytes($myvar{'innodb_buffer_pool_size'})."/".hr_bytes($enginestats{'InnoDB'})."\n"; + push(@adjvars,"innodb_buffer_pool_size (>= ".hr_bytes_rnd($enginestats{'InnoDB'}).")"); + } + if (defined $mystat{'Innodb_log_waits'} && $mystat{'Innodb_log_waits'} > 0) { + badprint "InnoDB log waits: ".$mystat{'Innodb_log_waits'}; + push(@adjvars,"innodb_log_buffer_size (>= ".hr_bytes_rnd($myvar{'innodb_log_buffer_size'}).")"); + } else { + goodprint "InnoDB log waits: ".$mystat{'Innodb_log_waits'}; + } + } +} + +# 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_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/mysql/lib/puppet/parser/functions/mysql_deepmerge.rb b/mysql/lib/puppet/parser/functions/mysql_deepmerge.rb new file mode 100644 index 000000000..aca9c7a3d --- /dev/null +++ b/mysql/lib/puppet/parser/functions/mysql_deepmerge.rb @@ -0,0 +1,58 @@ +module Puppet::Parser::Functions + newfunction(:mysql_deepmerge, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Recursively merges two or more hashes together and returns the resulting hash. + + For example: + + $hash1 = {'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } } + $hash2 = {'two' => 'dos', 'three' => { 'five' => 5 } } + $merged_hash = mysql_deepmerge($hash1, $hash2) + # The resulting hash is equivalent to: + # $merged_hash = { 'one' => 1, 'two' => 'dos', 'three' => { 'four' => 4, 'five' => 5 } } + + When there is a duplicate key that is a hash, they are recursively merged. + When there is a duplicate key that is not a hash, the key in the rightmost hash will "win." + When there are conficting uses of dashes and underscores in two keys (which mysql would otherwise equate), + the rightmost style will win. + + ENDHEREDOC + + if args.length < 2 + raise Puppet::ParseError, ("mysql_deepmerge(): wrong number of arguments (#{args.length}; must be at least 2)") + end + + result = Hash.new + args.each do |arg| + next if arg.is_a? String and arg.empty? # empty string is synonym for puppet's undef + # If the argument was not a hash, skip it. + unless arg.is_a?(Hash) + raise Puppet::ParseError, "mysql_deepmerge: unexpected argument type #{arg.class}, only expects hash arguments" + end + + # Now we have to traverse our hash assigning our non-hash values + # to the matching keys in our result while following our hash values + # and repeating the process. + overlay( result, arg ) + end + return( result ) + end +end + +def has_normalized!(hash, key) + return true if hash.has_key?( key ) + return false unless key.match(/-|_/) + other_key = key.include?('-') ? key.gsub( '-', '_' ) : key.gsub( '_', '-' ) + return false unless hash.has_key?( other_key ) + hash[key] = hash.delete( other_key ) + return true; +end + +def overlay( hash1, hash2 ) + hash2.each do |key, value| + if(has_normalized!( hash1, key ) and value.is_a?(Hash) and hash1[key].is_a?(Hash)) + overlay( hash1[key], value ) + else + hash1[key] = value + end + end +end diff --git a/mysql/lib/puppet/parser/functions/mysql_password.rb b/mysql/lib/puppet/parser/functions/mysql_password.rb new file mode 100644 index 000000000..f057a3139 --- /dev/null +++ b/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/mysql/lib/puppet/parser/functions/mysql_strip_hash.rb b/mysql/lib/puppet/parser/functions/mysql_strip_hash.rb new file mode 100644 index 000000000..8e850d02a --- /dev/null +++ b/mysql/lib/puppet/parser/functions/mysql_strip_hash.rb @@ -0,0 +1,21 @@ +module Puppet::Parser::Functions + newfunction(:mysql_strip_hash, :type => :rvalue, :arity => 1, :doc => <<-EOS +TEMPORARY FUNCTION: EXPIRES 2014-03-10 +When given a hash this function strips out all blank entries. +EOS + ) do |args| + + hash = args[0] + unless hash.is_a?(Hash) + raise(Puppet::ParseError, 'mysql_strip_hash(): Requires hash to work with') + end + + # Filter out all the top level blanks. + hash.reject{|k,v| v == ''}.each do |k,v| + if v.is_a?(Hash) + v.reject!{|ki,vi| vi == '' } + end + end + + end +end diff --git a/mysql/lib/puppet/provider/database/mysql.rb b/mysql/lib/puppet/provider/database/mysql.rb new file mode 100644 index 000000000..ace742967 --- /dev/null +++ b/mysql/lib/puppet/provider/database/mysql.rb @@ -0,0 +1,41 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) +Puppet::Type.type(:database).provide(:mysql, :parent => Puppet::Provider::Mysql) do + desc 'Manages MySQL database.' + + defaultfor :kernel => 'Linux' + + optional_commands :mysql => 'mysql' + optional_commands :mysqladmin => 'mysqladmin' + + def self.instances + mysql([defaults_file, '-NBe', 'show databases'].compact).split("\n").collect do |name| + new(:name => name) + end + end + + def create + mysql([defaults_file, '-NBe', "create database `#{@resource[:name]}` character set #{resource[:charset]}"].compact) + end + + def destroy + mysqladmin([defaults_file, '-f', 'drop', @resource[:name]].compact) + end + + def charset + mysql([defaults_file, '-NBe', "show create database `#{resource[:name]}`"].compact).match(/.*?(\S+)\s(?:COLLATE.*)?\*\//)[1] + end + + def charset=(value) + mysql([defaults_file, '-NBe', "alter database `#{resource[:name]}` CHARACTER SET #{value}"].compact) + end + + def exists? + begin + mysql([defaults_file, '-NBe', 'show databases'].compact).match(/^#{@resource[:name]}$/) + rescue => e + debug(e.message) + return nil + end + end + +end diff --git a/mysql/lib/puppet/provider/database_grant/mysql.rb b/mysql/lib/puppet/provider/database_grant/mysql.rb new file mode 100644 index 000000000..eabc649c3 --- /dev/null +++ b/mysql/lib/puppet/provider/database_grant/mysql.rb @@ -0,0 +1,199 @@ +# 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 + +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) +Puppet::Type.type(:database_grant).provide(:mysql, :parent => Puppet::Provider::Mysql) do + + desc 'Uses mysql as database.' + + defaultfor :kernel => 'Linux' + + optional_commands :mysql => 'mysql' + optional_commands :mysqladmin => 'mysqladmin' + + def self.prefetch(resources) + @user_privs = query_user_privs + @db_privs = query_db_privs + end + + def self.user_privs + @user_privs || query_user_privs + end + + def self.db_privs + @db_privs || query_db_privs + end + + def user_privs + self.class.user_privs + end + + def db_privs + self.class.db_privs + end + + def self.query_user_privs + results = mysql([defaults_file, 'mysql', '-Be', 'describe user'].compact) + column_names = results.split(/\n/).map { |l| l.chomp.split(/\t/)[0] } + @user_privs = column_names.delete_if { |e| !( e =~/_priv$/) } + end + + def self.query_db_privs + results = mysql([defaults_file, 'mysql', '-Be', 'describe db'].compact) + column_names = results.split(/\n/).map { |l| l.chomp.split(/\t/)[0] } + @db_privs = column_names.delete_if { |e| !(e =~/_priv$/) } + end + + def mysql_flush + mysqladmin([defaults_file, 'flush-privileges'].compact) + end + + # this parses the + def split_name(string) + matches = /^([^@]*)@([^\/]*)(\/(.*))?$/.match(string).captures.compact + case matches.length + when 2 + { + :type => :user, + :user => matches[0], + :host => matches[1] + } + when 4 + { + :type => :db, + :user => matches[0], + :host => matches[1], + :db => matches[3] + } + end + end + + def create_row + unless @resource.should(:privileges).empty? + name = split_name(@resource[:name]) + case name[:type] + when :user + mysql([defaults_file, 'mysql', '-e', "INSERT INTO user (host, user) VALUES ('%s', '%s')" % [ + name[:host], name[:user], + ]].compact) + when :db + mysql([defaults_file, 'mysql', '-e', "INSERT INTO db (host, user, db) VALUES ('%s', '%s', '%s')" % [ + name[:host], name[:user], name[:db], + ]].compact) + end + mysql_flush + end + end + + def destroy + mysql([defaults_file, 'mysql', '-e', "REVOKE ALL ON '%s'.* FROM '%s@%s'" % [ @resource[:privileges], @resource[:database], @resource[:name], @resource[:host] ]].compact) + end + + def row_exists? + name = split_name(@resource[:name]) + fields = [:user, :host] + if name[:type] == :db + fields << :db + end + not mysql([defaults_file, 'mysql', '-NBe', "SELECT '1' FROM %s WHERE %s" % [ name[:type], fields.map do |f| "%s='%s'" % [f, name[f]] end.join(' AND ')]].compact).empty? + end + + def all_privs_set? + all_privs = case split_name(@resource[:name])[:type] + when :user + user_privs + when :db + db_privs + end + all_privs = all_privs.collect do |p| p.downcase end.sort.join('|') + privs = privileges.collect do |p| p.downcase end.sort.join('|') + + all_privs == privs + end + + def privileges + name = split_name(@resource[:name]) + privs = '' + + case name[:type] + when :user + privs = mysql([defaults_file, 'mysql', '-Be', "select * from mysql.user where user='%s' and host='%s'" % [ name[:user], name[:host] ]].compact) + when :db + privs = mysql([defaults_file, 'mysql', '-Be', "select * from mysql.db where user='%s' and host='%s' and db='%s'" % [ name[:user], name[:host], name[:db] ]].compact) + end + + if privs.match(/^$/) + privs = [] # no result, no privs + else + # returns a line with field names and a line with values, each tab-separated + privs = privs.split(/\n/).map! do |l| l.chomp.split(/\t/) end + # transpose the lines, so we have key/value pairs + privs = privs[0].zip(privs[1]) + privs = privs.select do |p| p[0].match(/_priv$/) and p[1] == 'Y' end + end + + privs.collect do |p| p[0] end + end + + def privileges=(privs) + unless row_exists? + create_row + end + + # puts "Setting privs: ", privs.join(", ") + name = split_name(@resource[:name]) + stmt = '' + where = '' + all_privs = [] + case name[:type] + when :user + stmt = 'update user set ' + where = " where user='%s' and host='%s'" % [ name[:user], name[:host] ] + all_privs = user_privs + when :db + stmt = 'update db set ' + where = " where user='%s' and host='%s' and db='%s'" % [ name[:user], name[:host], name[:db] ] + all_privs = db_privs + end + + if privs[0].downcase == 'all' + privs = all_privs + end + + # Downcase the requested priviliges for case-insensitive selection + # we don't map! here because the all_privs object has to remain in + # the same case the DB gave it to us in + privs = privs.map { |p| p.downcase } + + # puts "stmt:", stmt + set = all_privs.collect do |p| "%s = '%s'" % [p, privs.include?(p.downcase) ? 'Y' : 'N'] end.join(', ') + # puts "set:", set + stmt = stmt << set << where + + validate_privs privs, all_privs + mysql([defaults_file, 'mysql', '-Be', stmt].compact) + mysql_flush + end + + def validate_privs(set_privs, all_privs) + all_privs = all_privs.collect { |p| p.downcase } + set_privs = set_privs.collect { |p| p.downcase } + invalid_privs = Array.new + hints = Array.new + # Test each of the user provided privs to see if they exist in all_privs + set_privs.each do |priv| + invalid_privs << priv unless all_privs.include?(priv) + hints << "#{priv}_priv" if all_privs.include?("#{priv}_priv") + end + unless invalid_privs.empty? + # Print a decently helpful and gramatically correct error message + hints = "Did you mean '#{hints.join(',')}'?" unless hints.empty? + p = invalid_privs.size > 1 ? ['s', 'are not valid'] : ['', 'is not valid'] + detail = ["The privilege#{p[0]} '#{invalid_privs.join(',')}' #{p[1]}."] + fail [detail, hints].join(' ') + end + end + +end diff --git a/mysql/lib/puppet/provider/database_user/mysql.rb b/mysql/lib/puppet/provider/database_user/mysql.rb new file mode 100644 index 000000000..71e76d5c3 --- /dev/null +++ b/mysql/lib/puppet/provider/database_user/mysql.rb @@ -0,0 +1,65 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) +Puppet::Type.type(:database_user).provide(:mysql, :parent => Puppet::Provider::Mysql) do + + desc 'manage users for a mysql database.' + + defaultfor :kernel => 'Linux' + + commands :mysql => 'mysql' + commands :mysqladmin => 'mysqladmin' + + def self.instances + users = mysql([defaults_file, 'mysql', '-BNe' "select concat(User, '@',Host) as User from mysql.user"].compact).split("\n") + users.select{ |user| user =~ /.+@/ }.collect do |name| + new(:name => name) + end + end + + def create + merged_name = self.class.cmd_user(@resource[:name]) + password_hash = @resource.value(:password_hash) + max_user_connections = @resource.value(:max_user_connections) || 0 + + mysql([defaults_file, 'mysql', '-e', "grant usage on *.* to #{merged_name} identified by PASSWORD + '#{password_hash}' with max_user_connections #{max_user_connections}"].compact) + + exists? ? (return true) : (return false) + end + + def destroy + merged_name = self.class.cmd_user(@resource[:name]) + mysql([defaults_file, 'mysql', '-e', "drop user #{merged_name}"].compact) + + exists? ? (return false) : (return true) + end + + def password_hash + mysql([defaults_file, 'mysql', '-NBe', "select password from mysql.user where CONCAT(user, '@', host) = '#{@resource[:name]}'"].compact).chomp + end + + def password_hash=(string) + mysql([defaults_file, 'mysql', '-e', "SET PASSWORD FOR #{self.class.cmd_user(@resource[:name])} = '#{string}'"].compact) + + password_hash == string ? (return true) : (return false) + end + + def max_user_connections + mysql([defaults_file, "mysql", "-NBe", "select max_user_connections from mysql.user where CONCAT(user, '@', host) = '#{@resource[:name]}'"].compact).chomp + end + + def max_user_connections=(int) + mysql([defaults_file, "mysql", "-e", "grant usage on *.* to %s with max_user_connections #{int}" % [ self.class.cmd_user(@resource[:name])] ].compact).chomp + + max_user_connections == int ? (return true) : (return false) + end + + def exists? + not mysql([defaults_file, 'mysql', '-NBe', "select '1' from mysql.user where CONCAT(user, '@', host) = '%s'" % @resource.value(:name)].compact).empty? + end + + def flush + @property_hash.clear + mysqladmin([defaults_file, 'flush-privileges'].compact) + end + +end diff --git a/mysql/lib/puppet/provider/mysql.rb b/mysql/lib/puppet/provider/mysql.rb new file mode 100644 index 000000000..9b34ca00e --- /dev/null +++ b/mysql/lib/puppet/provider/mysql.rb @@ -0,0 +1,70 @@ +class Puppet::Provider::Mysql < Puppet::Provider + + # Without initvars commands won't work. + initvars + commands :mysql => 'mysql' + commands :mysqladmin => 'mysqladmin' + + # Optional defaults file + def self.defaults_file + if File.file?("#{Facter.value(:root_home)}/.my.cnf") + "--defaults-extra-file=#{Facter.value(:root_home)}/.my.cnf" + else + nil + end + end + + def defaults_file + self.class.defaults_file + end + + def self.users + mysql([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"].compact).split("\n") + end + + # Take root@localhost and munge it to 'root'@'localhost' + def self.cmd_user(user) + "'#{user.sub('@', "'@'")}'" + end + + # Take root.* and return ON `root`.* + def self.cmd_table(table) + table_string = '' + + # We can't escape *.* so special case this. + if table == '*.*' + table_string << '*.*' + # Special case also for PROCEDURES + elsif table.start_with?('PROCEDURE ') + table_string << table.sub(/^PROCEDURE (.*)(\..*)/, 'PROCEDURE `\1`\2') + else + table_string << table.sub(/^(.*)(\..*)/, '`\1`\2') + end + table_string + end + + def self.cmd_privs(privileges) + if privileges.include?('ALL') + return 'ALL PRIVILEGES' + else + priv_string = '' + privileges.each do |priv| + priv_string << "#{priv}, " + end + end + # Remove trailing , from the last element. + priv_string.sub(/, $/, '') + end + + # Take in potential options and build up a query string with them. + def self.cmd_options(options) + option_string = '' + options.each do |opt| + if opt == 'GRANT' + option_string << ' WITH GRANT OPTION' + end + end + option_string + end + +end diff --git a/mysql/lib/puppet/provider/mysql_database/mysql.rb b/mysql/lib/puppet/provider/mysql_database/mysql.rb new file mode 100644 index 000000000..213e0de7a --- /dev/null +++ b/mysql/lib/puppet/provider/mysql_database/mysql.rb @@ -0,0 +1,68 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) +Puppet::Type.type(:mysql_database).provide(:mysql, :parent => Puppet::Provider::Mysql) do + desc 'Manages MySQL databases.' + + commands :mysql => 'mysql' + + def self.instances + mysql([defaults_file, '-NBe', 'show databases'].compact).split("\n").collect do |name| + attributes = {} + mysql([defaults_file, '-NBe', "show variables like '%_database'", name].compact).split("\n").each do |line| + k,v = line.split(/\s/) + attributes[k] = v + end + new(:name => name, + :ensure => :present, + :charset => attributes['character_set_database'], + :collate => attributes['collation_database'] + ) + end + end + + # We iterate over each mysql_database entry in the catalog and compare it against + # the contents of the property_hash generated by self.instances + def self.prefetch(resources) + databases = instances + resources.keys.each do |database| + if provider = databases.find { |db| db.name == database } + resources[database].provider = provider + end + end + end + + def create + mysql([defaults_file, '-NBe', "create database if not exists `#{@resource[:name]}` character set #{@resource[:charset]} collate #{@resource[:collate]}"].compact) + + @property_hash[:ensure] = :present + @property_hash[:charset] = @resource[:charset] + @property_hash[:collate] = @resource[:collate] + + exists? ? (return true) : (return false) + end + + def destroy + mysql([defaults_file, '-NBe', "drop database if exists `#{@resource[:name]}`"].compact) + + @property_hash.clear + exists? ? (return false) : (return true) + end + + def exists? + @property_hash[:ensure] == :present || false + end + + mk_resource_methods + + def charset=(value) + mysql([defaults_file, '-NBe', "alter database `#{resource[:name]}` CHARACTER SET #{value}"].compact) + @property_hash[:charset] = value + charset == value ? (return true) : (return false) + end + + def collate=(value) + mysql([defaults_file, '-NBe', "alter database `#{resource[:name]}` COLLATE #{value}"].compact) + @property_hash[:collate] = value + collate == value ? (return true) : (return false) + end + +end diff --git a/mysql/lib/puppet/provider/mysql_grant/mysql.rb b/mysql/lib/puppet/provider/mysql_grant/mysql.rb new file mode 100644 index 000000000..3fe691d56 --- /dev/null +++ b/mysql/lib/puppet/provider/mysql_grant/mysql.rb @@ -0,0 +1,135 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) +Puppet::Type.type(:mysql_grant).provide(:mysql, :parent => Puppet::Provider::Mysql) do + + desc 'Set grants for users in MySQL.' + + def self.instances + instances = [] + users.select{ |user| user =~ /.+@/ }.collect do |user| + user_string = self.cmd_user(user) + query = "SHOW GRANTS FOR #{user_string};" + begin + grants = mysql([defaults_file, "-NBe", query].compact) + rescue Puppet::ExecutionFailure => e + # Silently ignore users with no grants. Can happen e.g. if user is + # defined with fqdn and server is run with skip-name-resolve. Example: + # Default root user created by mysql_install_db on a host with fqdn + # of myhost.mydomain.my: root@myhost.mydomain.my, when MySQL is started + # with --skip-name-resolve. + if e.inspect =~ /There is no such grant defined for user/ + next + else + raise Puppet::Error, "#mysql had an error -> #{e.inspect}" + end + end + # Once we have the list of grants generate entries for each. + grants.each_line do |grant| + # Match the munges we do in the type. + munged_grant = grant.delete("'").delete("`") + # Matching: GRANT (SELECT, UPDATE) PRIVILEGES ON (*.*) TO ('root')@('127.0.0.1') (WITH GRANT OPTION) + if match = munged_grant.match(/^GRANT\s(.+)\sON\s(.+)\sTO\s(.*)@(.*?)(\s.*)$/) + privileges, table, user, host, rest = match.captures + # Once we split privileges up on the , we need to make sure we + # shortern ALL PRIVILEGES to just all. + stripped_privileges = privileges.split(',').map do |priv| + priv == 'ALL PRIVILEGES' ? 'ALL' : priv.lstrip.rstrip + end + # Same here, but to remove OPTION leaving just GRANT. + options = ['GRANT'] if rest.match(/WITH\sGRANT\sOPTION/) + # fix double backslash that MySQL prints, so resources match + table.gsub!("\\\\", "\\") + # We need to return an array of instances so capture these + instances << new( + :name => "#{user}@#{host}/#{table}", + :ensure => :present, + :privileges => stripped_privileges.sort, + :table => table, + :user => "#{user}@#{host}", + :options => options + ) + end + end + end + return instances + end + + def self.prefetch(resources) + users = instances + resources.keys.each do |name| + if provider = users.find { |user| user.name == name } + resources[name].provider = provider + end + end + end + + def grant(user, table, privileges, options) + user_string = self.class.cmd_user(user) + priv_string = self.class.cmd_privs(privileges) + table_string = self.class.cmd_table(table) + query = "GRANT #{priv_string}" + query << " ON #{table_string}" + query << " TO #{user_string}" + query << self.class.cmd_options(options) unless options.nil? + mysql([defaults_file, '-e', query].compact) + end + + def create + grant(@resource[:user], @resource[:table], @resource[:privileges], @resource[:options]) + + @property_hash[:ensure] = :present + @property_hash[:table] = @resource[:table] + @property_hash[:user] = @resource[:user] + @property_hash[:options] = @resource[:options] if @resource[:options] + @property_hash[:privileges] = @resource[:privileges] + + exists? ? (return true) : (return false) + end + + def revoke(user, table) + user_string = self.class.cmd_user(user) + table_string = self.class.cmd_table(table) + + query = "REVOKE ALL ON #{table_string} FROM #{user_string}" + mysql([defaults_file, '-e', query].compact) + # revoke grant option needs to be a extra query, because + # "REVOKE ALL PRIVILEGES, GRANT OPTION [..]" is only valid mysql syntax + # if no ON clause is used. + query = "REVOKE GRANT OPTION ON #{table_string} FROM #{user_string}" + mysql([defaults_file, '-e', query].compact) + end + + def destroy + revoke(@property_hash[:user], @property_hash[:table]) + @property_hash.clear + + exists? ? (return false) : (return true) + end + + def exists? + @property_hash[:ensure] == :present || false + end + + def flush + @property_hash.clear + mysql([defaults_file, '-NBe', 'FLUSH PRIVILEGES'].compact) + end + + mk_resource_methods + + def privileges=(privileges) + revoke(@property_hash[:user], @property_hash[:table]) + grant(@property_hash[:user], @property_hash[:table], privileges, @property_hash[:options]) + @property_hash[:privileges] = privileges + + self.privileges + end + + def options=(options) + revoke(@property_hash[:user], @property_hash[:table]) + grant(@property_hash[:user], @property_hash[:table], @property_hash[:privileges], options) + @property_hash[:options] = options + + self.options + end + +end diff --git a/mysql/lib/puppet/provider/mysql_user/mysql.rb b/mysql/lib/puppet/provider/mysql_user/mysql.rb new file mode 100644 index 000000000..066ea0b00 --- /dev/null +++ b/mysql/lib/puppet/provider/mysql_user/mysql.rb @@ -0,0 +1,115 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) +Puppet::Type.type(:mysql_user).provide(:mysql, :parent => Puppet::Provider::Mysql) do + + desc 'manage users for a mysql database.' + commands :mysql => 'mysql' + + # Build a property_hash containing all the discovered information about MySQL + # users. + def self.instances + users = mysql([defaults_file, '-NBe', + "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"].compact).split("\n") + # To reduce the number of calls to MySQL we collect all the properties in + # one big swoop. + users.collect do |name| + query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" + @max_user_connections, @max_connections_per_hour, @max_queries_per_hour, + @max_updates_per_hour, @password = mysql([defaults_file, "-NBe", query].compact).split(/\s/) + + new(:name => name, + :ensure => :present, + :password_hash => @password, + :max_user_connections => @max_user_connections, + :max_connections_per_hour => @max_connections_per_hour, + :max_queries_per_hour => @max_queries_per_hour, + :max_updates_per_hour => @max_updates_per_hour + ) + end + end + + # We iterate over each mysql_user entry in the catalog and compare it against + # the contents of the property_hash generated by self.instances + def self.prefetch(resources) + users = instances + resources.keys.each do |name| + if provider = users.find { |user| user.name == name } + resources[name].provider = provider + end + end + end + + def create + merged_name = @resource[:name].sub('@', "'@'") + password_hash = @resource.value(:password_hash) + max_user_connections = @resource.value(:max_user_connections) || 0 + max_connections_per_hour = @resource.value(:max_connections_per_hour) || 0 + max_queries_per_hour = @resource.value(:max_queries_per_hour) || 0 + max_updates_per_hour = @resource.value(:max_updates_per_hour) || 0 + + mysql([defaults_file, '-e', "GRANT USAGE ON *.* TO '#{merged_name}' IDENTIFIED BY PASSWORD '#{password_hash}' WITH MAX_USER_CONNECTIONS #{max_user_connections} MAX_CONNECTIONS_PER_HOUR #{max_connections_per_hour} MAX_QUERIES_PER_HOUR #{max_queries_per_hour} MAX_UPDATES_PER_HOUR #{max_updates_per_hour}"].compact) + + @property_hash[:ensure] = :present + @property_hash[:password_hash] = password_hash + @property_hash[:max_user_connections] = max_user_connections + @property_hash[:max_connections_per_hour] = max_connections_per_hour + @property_hash[:max_queries_per_hour] = max_queries_per_hour + @property_hash[:max_updates_per_hour] = max_updates_per_hour + + exists? ? (return true) : (return false) + end + + def destroy + merged_name = @resource[:name].sub('@', "'@'") + mysql([defaults_file, '-e', "DROP USER '#{merged_name}'"].compact) + + @property_hash.clear + exists? ? (return false) : (return true) + end + + def exists? + @property_hash[:ensure] == :present || false + end + + ## + ## MySQL user properties + ## + + # Generates method for all properties of the property_hash + mk_resource_methods + + def password_hash=(string) + merged_name = self.class.cmd_user(@resource[:name]) + mysql([defaults_file, '-e', "SET PASSWORD FOR #{merged_name} = '#{string}'"].compact) + + password_hash == string ? (return true) : (return false) + end + + def max_user_connections=(int) + merged_name = self.class.cmd_user(@resource[:name]) + mysql([defaults_file, '-e', "GRANT USAGE ON *.* TO #{merged_name} WITH MAX_USER_CONNECTIONS #{int}"].compact).chomp + + max_user_connections == int ? (return true) : (return false) + end + + def max_connections_per_hour=(int) + merged_name = self.class.cmd_user(@resource[:name]) + mysql([defaults_file, '-e', "GRANT USAGE ON *.* TO #{merged_name} WITH MAX_CONNECTIONS_PER_HOUR #{int}"].compact).chomp + + max_connections_per_hour == int ? (return true) : (return false) + end + + def max_queries_per_hour=(int) + merged_name = self.class.cmd_user(@resource[:name]) + mysql([defaults_file, '-e', "GRANT USAGE ON *.* TO #{merged_name} WITH MAX_QUERIES_PER_HOUR #{int}"].compact).chomp + + max_queries_per_hour == int ? (return true) : (return false) + end + + def max_updates_per_hour=(int) + merged_name = self.class.cmd_user(@resource[:name]) + mysql([defaults_file, '-e', "GRANT USAGE ON *.* TO #{merged_name} WITH MAX_UPDATES_PER_HOUR #{int}"].compact).chomp + + max_updates_per_hour == int ? (return true) : (return false) + end + +end diff --git a/mysql/lib/puppet/type/database.rb b/mysql/lib/puppet/type/database.rb new file mode 100644 index 000000000..b02fb1099 --- /dev/null +++ b/mysql/lib/puppet/type/database.rb @@ -0,0 +1,21 @@ +# 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.' + validate do |value| + Puppet.warning("database has been deprecated in favor of mysql_database.") + true + end + end + + newproperty(:charset) do + desc 'The characterset to use for a database' + defaultto :utf8 + newvalue(/^\S+$/) + end + +end diff --git a/mysql/lib/puppet/type/database_grant.rb b/mysql/lib/puppet/type/database_grant.rb new file mode 100644 index 000000000..7fdad8231 --- /dev/null +++ b/mysql/lib/puppet/type/database_grant.rb @@ -0,0 +1,79 @@ +# 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' + validate do |value| + Puppet.warning("database_grant has been deprecated in favor of mysql_grant.") + true + end + 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/mysql/lib/puppet/type/database_user.rb b/mysql/lib/puppet/type/database_user.rb new file mode 100644 index 000000000..24abc70d5 --- /dev/null +++ b/mysql/lib/puppet/type/database_user.rb @@ -0,0 +1,36 @@ +# 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 privileges' + + ensurable + + newparam(:name, :namevar=>true) do + desc "The name of the user. This uses the 'username@hostname' or username@hostname." + validate do |value| + Puppet.warning("database has been deprecated in favor of mysql_user.") + # 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 + + munge do |value| + user_part, host_part = value.split('@') + "#{user_part}@#{host_part.downcase}" + end + end + + newproperty(:password_hash) do + desc 'The password hash of the user. Use mysql_password() for creating such a hash.' + newvalue(/\w+/) + end + + newproperty(:max_user_connections) do + desc "Max concurrent connections for the user. 0 means no (or global) limit." + newvalue(/\d+/) + end + +end diff --git a/mysql/lib/puppet/type/mysql_database.rb b/mysql/lib/puppet/type/mysql_database.rb new file mode 100644 index 000000000..3e8518c96 --- /dev/null +++ b/mysql/lib/puppet/type/mysql_database.rb @@ -0,0 +1,22 @@ +Puppet::Type.newtype(:mysql_database) do + @doc = 'Manage MySQL databases.' + + ensurable + + newparam(:name, :namevar => true) do + desc 'The name of the MySQL database to manage.' + end + + newproperty(:charset) do + desc 'The CHARACTER SET setting for the database' + defaultto :utf8 + newvalue(/^\S+$/) + end + + newproperty(:collate) do + desc 'The COLLATE setting for the database' + defaultto :utf8_general_ci + newvalue(/^\S+$/) + end + +end diff --git a/mysql/lib/puppet/type/mysql_grant.rb b/mysql/lib/puppet/type/mysql_grant.rb new file mode 100644 index 000000000..a268e4cb5 --- /dev/null +++ b/mysql/lib/puppet/type/mysql_grant.rb @@ -0,0 +1,73 @@ +# This has to be a separate type to enable collecting +Puppet::Type.newtype(:mysql_grant) do + @doc = "Manage a MySQL user's rights." + ensurable + + autorequire(:file) { '/root/.my.cnf' } + + def initialize(*args) + super + # Forcibly munge any privilege with 'ALL' in the array to exist of just + # 'ALL'. This can't be done in the munge in the property as that iterates + # over the array and there's no way to replace the entire array before it's + # returned to the provider. + if self[:ensure] == :present and Array(self[:privileges]).count > 1 and self[:privileges].to_s.include?('ALL') + self[:privileges] = 'ALL' + end + # Sort the privileges array in order to ensure the comparision in the provider + # self.instances method match. Otherwise this causes it to keep resetting the + # privileges. + self[:privileges] = Array(self[:privileges]).map(&:upcase).uniq.reject{|k| k == 'GRANT' or k == 'GRANT OPTION'}.sort! + end + + validate do + fail('privileges parameter is required.') if self[:ensure] == :present and self[:privileges].nil? + fail('table parameter is required.') if self[:ensure] == :present and self[:table].nil? + fail('user parameter is required.') if self[:ensure] == :present and self[:user].nil? + fail('name must match user and table parameters') if self[:name] != "#{self[:user]}/#{self[:table]}" + end + + newparam(:name, :namevar => true) do + desc 'Name to describe the grant.' + + munge do |value| + value.delete("'") + end + end + + newproperty(:privileges, :array_matching => :all) do + desc 'Privileges for user' + + munge do |value| + value.upcase + end + end + + newproperty(:table) do + desc 'Table to apply privileges to.' + + munge do |value| + value.delete("`") + end + + newvalues(/.*\..*/,/@/) + end + + newproperty(:user) do + desc 'User to operate on.' + 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 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(:options, :array_matching => :all) do + desc 'Options to grant.' + end + +end diff --git a/mysql/lib/puppet/type/mysql_user.rb b/mysql/lib/puppet/type/mysql_user.rb new file mode 100644 index 000000000..2d059ce3f --- /dev/null +++ b/mysql/lib/puppet/type/mysql_user.rb @@ -0,0 +1,50 @@ +# This has to be a separate type to enable collecting +Puppet::Type.newtype(:mysql_user) do + @doc = 'Manage a MySQL user. This includes management of users password as well as privileges.' + + 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 + + munge do |value| + user_part, host_part = value.split('@') + "#{user_part}@#{host_part.downcase}" + end + end + + newproperty(:password_hash) do + desc 'The password hash of the user. Use mysql_password() for creating such a hash.' + newvalue(/\w+/) + end + + newproperty(:max_user_connections) do + desc "Max concurrent connections for the user. 0 means no (or global) limit." + newvalue(/\d+/) + end + + newproperty(:max_connections_per_hour) do + desc "Max connections per hour for the user. 0 means no (or global) limit." + newvalue(/\d+/) + end + + newproperty(:max_queries_per_hour) do + desc "Max queries per hour for the user. 0 means no (or global) limit." + newvalue(/\d+/) + end + + newproperty(:max_updates_per_hour) do + desc "Max updates per hour for the user. 0 means no (or global) limit." + newvalue(/\d+/) + end + +end diff --git a/mysql/manifests/backup.pp b/mysql/manifests/backup.pp new file mode 100644 index 000000000..680a5744d --- /dev/null +++ b/mysql/manifests/backup.pp @@ -0,0 +1,31 @@ +# Deprecated class +class mysql::backup ( + $backupuser, + $backuppassword, + $backupdir, + $backupcompress = true, + $backuprotate = 30, + $delete_before_dump = false, + $backupdatabases = [], + $file_per_database = false, + $ensure = 'present', + $time = ['23', '5'], +) { + + crit("This class has been deprecated and callers should directly call + mysql::server::backup now.") + + class { 'mysql::server::backup': + ensure => $ensure, + backupuser => $backupuser, + backuppassword => $backuppassword, + backupdir => $backupdir, + backupcompress => $backupcompress, + backuprotate => $backuprotate, + delete_before_dump => $delete_before_dump, + backupdatabases => $backupdatabases, + file_per_database => $file_per_database, + time => $time, + } + +} diff --git a/mysql/manifests/bindings.pp b/mysql/manifests/bindings.pp new file mode 100644 index 000000000..40aa51d74 --- /dev/null +++ b/mysql/manifests/bindings.pp @@ -0,0 +1,56 @@ +# See README.md. +class mysql::bindings ( + # Boolean to determine if we should include the classes. + $java_enable = false, + $perl_enable = false, + $php_enable = false, + $python_enable = false, + $ruby_enable = false, + $client_dev = false, + $daemon_dev = false, + # Settings for the various classes. + $java_package_ensure = $mysql::params::java_package_ensure, + $java_package_name = $mysql::params::java_package_name, + $java_package_provider = $mysql::params::java_package_provider, + $perl_package_ensure = $mysql::params::perl_package_ensure, + $perl_package_name = $mysql::params::perl_package_name, + $perl_package_provider = $mysql::params::perl_package_provider, + $php_package_ensure = $mysql::params::php_package_ensure, + $php_package_name = $mysql::params::php_package_name, + $php_package_provider = $mysql::params::php_package_provider, + $python_package_ensure = $mysql::params::python_package_ensure, + $python_package_name = $mysql::params::python_package_name, + $python_package_provider = $mysql::params::python_package_provider, + $ruby_package_ensure = $mysql::params::ruby_package_ensure, + $ruby_package_name = $mysql::params::ruby_package_name, + $ruby_package_provider = $mysql::params::ruby_package_provider, + $client_dev_package_ensure = $mysql::params::client_dev_package_ensure, + $client_dev_package_name = $mysql::params::client_dev_package_name, + $client_dev_package_provider = $mysql::params::client_dev_package_provider, + $daemon_dev_package_ensure = $mysql::params::daemon_dev_package_ensure, + $daemon_dev_package_name = $mysql::params::daemon_dev_package_name, + $daemon_dev_package_provider = $mysql::params::daemon_dev_package_provider +) inherits mysql::params { + + case $::osfamily { + 'Archlinux': { + if $java_enable { fatal("::mysql::bindings::java cannot be managed by puppet on ${::osfamily} as it is not in official repositories. Please disable java mysql binding.") } + if $perl_enable { include '::mysql::bindings::perl' } + if $php_enable { warn("::mysql::bindings::php does not need to be managed by puppet on ${::osfamily} as it is included in mysql package by default.") } + if $python_enable { include '::mysql::bindings::python' } + if $ruby_enable { fatal("::mysql::bindings::ruby cannot be managed by puppet on ${::osfamily} as it is not in official repositories. Please disable ruby mysql binding.") } + } + + default: { + if $java_enable { include '::mysql::bindings::java' } + if $perl_enable { include '::mysql::bindings::perl' } + if $php_enable { include '::mysql::bindings::php' } + if $python_enable { include '::mysql::bindings::python' } + if $ruby_enable { include '::mysql::bindings::ruby' } + } + } + + if $client_dev { include '::mysql::bindings::client_dev' } + if $daemon_dev { include '::mysql::bindings::daemon_dev' } + +} diff --git a/mysql/manifests/bindings/client_dev.pp b/mysql/manifests/bindings/client_dev.pp new file mode 100644 index 000000000..f8275039a --- /dev/null +++ b/mysql/manifests/bindings/client_dev.pp @@ -0,0 +1,14 @@ +# Private class +class mysql::bindings::client_dev { + + if $mysql::bindings::client_dev_package_name { + package { 'mysql-client_dev': + ensure => $mysql::bindings::client_dev_package_ensure, + name => $mysql::bindings::client_dev_package_name, + provider => $mysql::bindings::client_dev_package_provider, + } + } else { + warning("No MySQL client development package configured for ${::operatingsystem}.") + } + +} \ No newline at end of file diff --git a/mysql/manifests/bindings/daemon_dev.pp b/mysql/manifests/bindings/daemon_dev.pp new file mode 100644 index 000000000..fe2db53ee --- /dev/null +++ b/mysql/manifests/bindings/daemon_dev.pp @@ -0,0 +1,14 @@ +# Private class +class mysql::bindings::daemon_dev { + + if $mysql::bindings::daemon_dev_package_name { + package { 'mysql-daemon_dev': + ensure => $mysql::bindings::daemon_dev_package_ensure, + name => $mysql::bindings::daemon_dev_package_name, + provider => $mysql::bindings::daemon_dev_package_provider, + } + } else { + warning("No MySQL daemon development package configured for ${::operatingsystem}.") + } + +} \ No newline at end of file diff --git a/mysql/manifests/bindings/java.pp b/mysql/manifests/bindings/java.pp new file mode 100644 index 000000000..d08083733 --- /dev/null +++ b/mysql/manifests/bindings/java.pp @@ -0,0 +1,10 @@ +# Private class +class mysql::bindings::java { + + package { 'mysql-connector-java': + ensure => $mysql::bindings::java_package_ensure, + name => $mysql::bindings::java_package_name, + provider => $mysql::bindings::java_package_provider, + } + +} diff --git a/mysql/manifests/bindings/perl.pp b/mysql/manifests/bindings/perl.pp new file mode 100644 index 000000000..58c76f4b3 --- /dev/null +++ b/mysql/manifests/bindings/perl.pp @@ -0,0 +1,10 @@ +# Private class +class mysql::bindings::perl { + + package{ 'perl_mysql': + ensure => $mysql::bindings::perl_package_ensure, + name => $mysql::bindings::perl_package_name, + provider => $mysql::bindings::perl_package_provider, + } + +} diff --git a/mysql/manifests/bindings/php.pp b/mysql/manifests/bindings/php.pp new file mode 100644 index 000000000..81d08d3aa --- /dev/null +++ b/mysql/manifests/bindings/php.pp @@ -0,0 +1,10 @@ +# Private class: See README.md +class mysql::bindings::php { + + package { 'php-mysql': + ensure => $mysql::bindings::php_package_ensure, + name => $mysql::bindings::php_package_name, + provider => $mysql::bindings::php_package_provider, + } + +} diff --git a/mysql/manifests/bindings/python.pp b/mysql/manifests/bindings/python.pp new file mode 100644 index 000000000..96a3882b3 --- /dev/null +++ b/mysql/manifests/bindings/python.pp @@ -0,0 +1,10 @@ +# Private class +class mysql::bindings::python { + + package { 'python-mysqldb': + ensure => $mysql::bindings::python_package_ensure, + name => $mysql::bindings::python_package_name, + provider => $mysql::bindings::python_package_provider, + } + +} diff --git a/mysql/manifests/bindings/ruby.pp b/mysql/manifests/bindings/ruby.pp new file mode 100644 index 000000000..f916f54c2 --- /dev/null +++ b/mysql/manifests/bindings/ruby.pp @@ -0,0 +1,10 @@ +# Private class +class mysql::bindings::ruby { + + package{ 'ruby_mysql': + ensure => $mysql::bindings::ruby_package_ensure, + name => $mysql::bindings::ruby_package_name, + provider => $mysql::bindings::ruby_package_provider, + } + +} diff --git a/mysql/manifests/client.pp b/mysql/manifests/client.pp new file mode 100644 index 000000000..59487f700 --- /dev/null +++ b/mysql/manifests/client.pp @@ -0,0 +1,27 @@ +# +class mysql::client ( + $bindings_enable = $mysql::params::bindings_enable, + $package_ensure = $mysql::params::client_package_ensure, + $package_name = $mysql::params::client_package_name, +) inherits mysql::params { + + include '::mysql::client::install' + + if $bindings_enable { + class { 'mysql::bindings': + java_enable => true, + perl_enable => true, + php_enable => true, + python_enable => true, + ruby_enable => true, + } + } + + + # Anchor pattern workaround to avoid resources of mysql::client::install to + # "float off" outside mysql::client + anchor { 'mysql::client::start': } -> + Class['mysql::client::install'] -> + anchor { 'mysql::client::end': } + +} diff --git a/mysql/manifests/client/install.pp b/mysql/manifests/client/install.pp new file mode 100644 index 000000000..a443f5320 --- /dev/null +++ b/mysql/manifests/client/install.pp @@ -0,0 +1,8 @@ +class mysql::client::install { + + package { 'mysql_client': + ensure => $mysql::client::package_ensure, + name => $mysql::client::package_name, + } + +} diff --git a/mysql/manifests/db.pp b/mysql/manifests/db.pp new file mode 100644 index 000000000..69e2081fb --- /dev/null +++ b/mysql/manifests/db.pp @@ -0,0 +1,60 @@ +# See README.md for details. +define mysql::db ( + $user, + $password, + $dbname = $name, + $charset = 'utf8', + $collate = 'utf8_general_ci', + $host = 'localhost', + $grant = 'ALL', + $sql = '', + $enforce_sql = false, + $ensure = 'present' +) { + #input validation + validate_re($ensure, '^(present|absent)$', + "${ensure} is not supported for ensure. Allowed values are 'present' and 'absent'.") + $table = "${dbname}.*" + + include '::mysql::client' + + $db_resource = { + ensure => $ensure, + charset => $charset, + collate => $collate, + provider => 'mysql', + require => [ Class['mysql::server'], Class['mysql::client'] ], + } + ensure_resource('mysql_database', $dbname, $db_resource) + + $user_resource = { + ensure => $ensure, + password_hash => mysql_password($password), + provider => 'mysql', + require => Class['mysql::server'], + } + ensure_resource('mysql_user', "${user}@${host}", $user_resource) + + if $ensure == 'present' { + mysql_grant { "${user}@${host}/${table}": + privileges => $grant, + provider => 'mysql', + user => "${user}@${host}", + table => $table, + require => [Mysql_database[$dbname], Mysql_user["${user}@${host}"], Class['mysql::server'] ], + } + + $refresh = ! $enforce_sql + + if $sql { + exec{ "${dbname}-import": + command => "/usr/bin/mysql ${dbname} < ${sql}", + logoutput => true, + environment => "HOME=${::root_home}", + refreshonly => $refresh, + require => Mysql_grant["${user}@${host}/${table}"], + subscribe => Mysql_database[$dbname], + } + } + } +} diff --git a/mysql/manifests/init.pp b/mysql/manifests/init.pp new file mode 100644 index 000000000..eba5c2063 --- /dev/null +++ b/mysql/manifests/init.pp @@ -0,0 +1,100 @@ +# +class mysql( + $basedir = '', + $bind_address = '', + $client_package_ensure = '', + $client_package_name = '', + $config_file = '', + $config_template = '', + $datadir = '', + $default_engine = '', + $etc_root_password = '', + $log_error = '', + $manage_config_file = '', + $manage_service = '', + $max_allowed_packet = '', + $max_connections = '', + $old_root_password = '', + $package_ensure = '', + $php_package_name = '', + $pidfile = '', + $port = '', + $purge_conf_dir = '', + $restart = '', + $root_group = '', + $root_password = '', + $server_package_name = '', + $service_name = '', + $service_provider = '', + $socket = '', + $ssl = '', + $ssl_ca = '', + $ssl_cert = '', + $ssl_key = '', + $tmpdir = '', + $attempt_compatibility_mode = false, +) { + + if $attempt_compatibility_mode { + notify { "An attempt has been made below to automatically apply your custom + settings to mysql::server. Please verify this works in a safe test + environment.": } + + $override_options = { + 'client' => { + 'port' => $port, + 'socket' => $socket + }, + 'mysqld_safe' => { + 'log_error' => $log_error, + 'socket' => $socket, + }, + 'mysqld' => { + 'basedir' => $basedir, + 'bind_address' => $bind_address, + 'datadir' => $datadir, + 'log_error' => $log_error, + 'max_allowed_packet' => $max_allowed_packet, + 'max_connections' => $max_connections, + 'pid_file' => $pidfile, + 'port' => $port, + 'socket' => $socket, + 'ssl-ca' => $ssl_ca, + 'ssl-cert' => $ssl_cert, + 'ssl-key' => $ssl_key, + 'tmpdir' => $tmpdir, + }, + 'mysqldump' => { + 'max_allowed_packet' => $max_allowed_packet, + }, + 'config_file' => $config_file, + 'etc_root_password' => $etc_root_password, + 'manage_config_file' => $manage_config_file, + 'old_root_password' => $old_root_password, + 'purge_conf_dir' => $purge_conf_dir, + 'restart' => $restart, + 'root_group' => $root_group, + 'root_password' => $root_password, + 'service_name' => $service_name, + 'ssl' => $ssl + } + $filtered_options = mysql_strip_hash($override_options) + validate_hash($filtered_options) + notify { $filtered_options: } + class { 'mysql::server': + override_options => $filtered_options, + } + + } else { + fail("ERROR: This class has been deprecated and the functionality moved + into mysql::server. If you run mysql::server without correctly calling + mysql:: server with the new override_options hash syntax you will revert + your MySQL to the stock settings. Do not proceed without removing this + class and using mysql::server correctly. + + If you are brave you may set attempt_compatibility_mode in this class which + attempts to automap the previous settings to appropriate calls to + mysql::server") + } + +} diff --git a/mysql/manifests/params.pp b/mysql/manifests/params.pp new file mode 100644 index 000000000..68742e643 --- /dev/null +++ b/mysql/manifests/params.pp @@ -0,0 +1,310 @@ +# Private class: See README.md. +class mysql::params { + + $manage_config_file = true + $old_root_password = '' + $purge_conf_dir = false + $restart = false + $root_password = 'UNSET' + $server_package_ensure = 'present' + $server_service_manage = true + $server_service_enabled = true + $client_package_ensure = 'present' + # mysql::bindings + $bindings_enable = false + $java_package_ensure = 'present' + $java_package_provider = undef + $perl_package_ensure = 'present' + $perl_package_provider = undef + $php_package_ensure = 'present' + $php_package_provider = undef + $python_package_ensure = 'present' + $python_package_provider = undef + $ruby_package_ensure = 'present' + $ruby_package_provider = undef + $client_dev_package_ensure = 'present' + $client_dev_package_provider = undef + $daemon_dev_package_ensure = 'present' + $daemon_dev_package_provider = undef + + + case $::osfamily { + 'RedHat': { + case $::operatingsystem { + 'Fedora': { + if is_integer($::operatingsystemrelease) and $::operatingsystemrelease >= 19 or $::operatingsystemrelease == 'Rawhide' { + $provider = 'mariadb' + } else { + $provider = 'mysql' + } + } + /^(RedHat|CentOS|Scientific)$/: { + if $::operatingsystemmajrelease >= 7 { + $provider = 'mariadb' + } else { + $provider = 'mysql' + } + } + default: { + $provider = 'mysql' + } + } + + if $provider == 'mariadb' { + $client_package_name = 'mariadb' + $server_package_name = 'mariadb-server' + $server_service_name = 'mariadb' + $log_error = '/var/log/mariadb/mariadb.log' + $config_file = '/etc/my.cnf.d/server.cnf' + # mariadb package by default has !includedir set in my.cnf to /etc/my.cnf.d + $includedir = undef + $pidfile = '/var/run/mariadb/mariadb.pid' + } else { + $client_package_name = 'mysql' + $server_package_name = 'mysql-server' + $server_service_name = 'mysqld' + $log_error = '/var/log/mysqld.log' + $config_file = '/etc/my.cnf' + $includedir = '/etc/my.cnf.d' + $pidfile = '/var/run/mysqld/mysqld.pid' + } + + $basedir = '/usr' + $datadir = '/var/lib/mysql' + $root_group = 'root' + $socket = '/var/lib/mysql/mysql.sock' + $ssl_ca = '/etc/mysql/cacert.pem' + $ssl_cert = '/etc/mysql/server-cert.pem' + $ssl_key = '/etc/mysql/server-key.pem' + $tmpdir = '/tmp' + # mysql::bindings + $java_package_name = 'mysql-connector-java' + $perl_package_name = 'perl-DBD-MySQL' + $php_package_name = 'php-mysql' + $python_package_name = 'MySQL-python' + $ruby_package_name = 'ruby-mysql' + $client_dev_package_name = undef + $daemon_dev_package_name = 'mysql-devel' + } + + 'Suse': { + $client_package_name = $::operatingsystem ? { + /OpenSuSE/ => 'mysql-community-server-client', + /(SLES|SLED)/ => 'mysql-client', + } + $server_package_name = $::operatingsystem ? { + /OpenSuSE/ => 'mysql-community-server', + /(SLES|SLED)/ => 'mysql', + } + $basedir = '/usr' + $config_file = '/etc/my.cnf' + $includedir = '/etc/my.cnf.d' + $datadir = '/var/lib/mysql' + $log_error = $::operatingsystem ? { + /OpenSuSE/ => '/var/log/mysql/mysqld.log', + /(SLES|SLED)/ => '/var/log/mysqld.log', + } + $pidfile = $::operatingsystem ? { + /OpenSuSE/ => '/var/run/mysql/mysqld.pid', + /(SLES|SLED)/ => '/var/lib/mysql/mysqld.pid', + } + $root_group = 'root' + $server_service_name = 'mysql' + $socket = $::operatingsystem ? { + /OpenSuSE/ => '/var/run/mysql/mysql.sock', + /(SLES|SLED)/ => '/var/lib/mysql/mysql.sock', + } + $ssl_ca = '/etc/mysql/cacert.pem' + $ssl_cert = '/etc/mysql/server-cert.pem' + $ssl_key = '/etc/mysql/server-key.pem' + $tmpdir = '/tmp' + # mysql::bindings + $java_package_name = 'mysql-connector-java' + $perl_package_name = 'perl-DBD-mysql' + $php_package_name = 'apache2-mod_php53' + $python_package_name = 'python-mysql' + $ruby_package_name = $::operatingsystem ? { + /OpenSuSE/ => 'rubygem-mysql', + /(SLES|SLED)/ => 'ruby-mysql', + } + $client_dev_package_name = 'libmysqlclient-devel' + $daemon_dev_package_name = 'mysql-devel' + } + + 'Debian': { + $client_package_name = 'mysql-client' + $server_package_name = 'mysql-server' + + $basedir = '/usr' + $config_file = '/etc/mysql/my.cnf' + $includedir = '/etc/mysql/conf.d' + $datadir = '/var/lib/mysql' + $log_error = '/var/log/mysql/error.log' + $pidfile = '/var/run/mysqld/mysqld.pid' + $root_group = 'root' + $server_service_name = 'mysql' + $socket = '/var/run/mysqld/mysqld.sock' + $ssl_ca = '/etc/mysql/cacert.pem' + $ssl_cert = '/etc/mysql/server-cert.pem' + $ssl_key = '/etc/mysql/server-key.pem' + $tmpdir = '/tmp' + # mysql::bindings + $java_package_name = 'libmysql-java' + $perl_package_name = 'libdbd-mysql-perl' + $php_package_name = 'php5-mysql' + $python_package_name = 'python-mysqldb' + $ruby_package_name = $::lsbdistcodename ? { + 'trusty' => 'ruby-mysql', + default => 'libmysql-ruby', + } + $client_dev_package_name = 'libmysqlclient-dev' + $daemon_dev_package_name = 'libmysqld-dev' + } + + 'Archlinux': { + $client_package_name = 'mariadb-clients' + $server_package_name = 'mariadb' + $basedir = '/usr' + $config_file = '/etc/mysql/my.cnf' + $datadir = '/var/lib/mysql' + $log_error = '/var/log/mysqld.log' + $pidfile = '/var/run/mysqld/mysqld.pid' + $root_group = 'root' + $server_service_name = 'mysqld' + $socket = '/var/lib/mysql/mysql.sock' + $ssl_ca = '/etc/mysql/cacert.pem' + $ssl_cert = '/etc/mysql/server-cert.pem' + $ssl_key = '/etc/mysql/server-key.pem' + $tmpdir = '/tmp' + # mysql::bindings + $java_package_name = 'mysql-connector-java' + $perl_package_name = 'perl-dbd-mysql' + $php_package_name = undef + $python_package_name = 'mysql-python' + $ruby_package_name = 'mysql-ruby' + } + + 'FreeBSD': { + $client_package_name = 'databases/mysql55-client' + $server_package_name = 'databases/mysql55-server' + $basedir = '/usr/local' + $config_file = '/var/db/mysql/my.cnf' + $includedir = '/var/db/mysql/my.cnf.d' + $datadir = '/var/db/mysql' + $log_error = "/var/db/mysql/${::hostname}.err" + $pidfile = '/var/db/mysql/mysql.pid' + $root_group = 'wheel' + $server_service_name = 'mysql-server' + $socket = '/tmp/mysql.sock' + $ssl_ca = undef + $ssl_cert = undef + $ssl_key = undef + $tmpdir = '/tmp' + # mysql::bindings + $java_package_name = 'databases/mysql-connector-java' + $perl_package_name = 'p5-DBD-mysql' + $php_package_name = 'php5-mysql' + $python_package_name = 'databases/py-MySQLdb' + $ruby_package_name = 'databases/ruby-mysql' + # The libraries installed by these packages are included in client and server packages, no installation required. + $client_dev_package_name = undef + $daemon_dev_package_name = undef + } + + default: { + case $::operatingsystem { + 'Amazon': { + $client_package_name = 'mysql' + $server_package_name = 'mysql-server' + $basedir = '/usr' + $config_file = '/etc/my.cnf' + $includedir = '/etc/my.cnf.d' + $datadir = '/var/lib/mysql' + $log_error = '/var/log/mysqld.log' + $pidfile = '/var/run/mysqld/mysqld.pid' + $root_group = 'root' + $server_service_name = 'mysqld' + $socket = '/var/lib/mysql/mysql.sock' + $ssl_ca = '/etc/mysql/cacert.pem' + $ssl_cert = '/etc/mysql/server-cert.pem' + $ssl_key = '/etc/mysql/server-key.pem' + $tmpdir = '/tmp' + # mysql::bindings + $java_package_name = 'mysql-connector-java' + $perl_package_name = 'perl-DBD-MySQL' + $php_package_name = 'php-mysql' + $python_package_name = 'MySQL-python' + $ruby_package_name = 'ruby-mysql' + # The libraries installed by these packages are included in client and server packages, no installation required. + $client_dev_package_name = undef + $daemon_dev_package_name = undef + } + + default: { + fail("Unsupported platform: puppetlabs-${module_name} currently doesn't support ${::osfamily} or ${::operatingsystem}") + } + } + } + } + + case $::operatingsystem { + 'Ubuntu': { + $server_service_provider = upstart + } + default: { + $server_service_provider = undef + } + } + + $default_options = { + 'client' => { + 'port' => '3306', + 'socket' => $mysql::params::socket, + }, + 'mysqld_safe' => { + 'nice' => '0', + 'log-error' => $mysql::params::log_error, + 'socket' => $mysql::params::socket, + }, + 'mysqld' => { + 'basedir' => $mysql::params::basedir, + 'bind-address' => '127.0.0.1', + 'datadir' => $mysql::params::datadir, + 'expire_logs_days' => '10', + 'key_buffer_size' => '16M', + 'log-error' => $mysql::params::log_error, + 'max_allowed_packet' => '16M', + 'max_binlog_size' => '100M', + 'max_connections' => '151', + 'myisam_recover' => 'BACKUP', + 'pid-file' => $mysql::params::pidfile, + 'port' => '3306', + 'query_cache_limit' => '1M', + 'query_cache_size' => '16M', + 'skip-external-locking' => true, + 'socket' => $mysql::params::socket, + 'ssl' => false, + 'ssl-ca' => $mysql::params::ssl_ca, + 'ssl-cert' => $mysql::params::ssl_cert, + 'ssl-key' => $mysql::params::ssl_key, + 'ssl-disable' => false, + 'thread_cache_size' => '8', + 'thread_stack' => '256K', + 'tmpdir' => $mysql::params::tmpdir, + 'user' => 'mysql', + }, + 'mysqldump' => { + 'max_allowed_packet' => '16M', + 'quick' => true, + 'quote-names' => true, + }, + 'isamchk' => { + 'key_buffer_size' => '16M', + }, + } + + ## Additional graceful failures + if $::osfamily == 'RedHat' and $::operatingsystemmajrelease == '4' { + fail("Unsupported platform: puppetlabs-${module_name} only supports RedHat 5.0 and beyond") + } +} diff --git a/mysql/manifests/server.pp b/mysql/manifests/server.pp new file mode 100644 index 000000000..2bd354053 --- /dev/null +++ b/mysql/manifests/server.pp @@ -0,0 +1,83 @@ +# Class: mysql::server: See README.md for documentation. +class mysql::server ( + $config_file = $mysql::params::config_file, + $includedir = $mysql::params::includedir, + $manage_config_file = $mysql::params::manage_config_file, + $old_root_password = $mysql::params::old_root_password, + $override_options = {}, + $package_ensure = $mysql::params::server_package_ensure, + $package_name = $mysql::params::server_package_name, + $purge_conf_dir = $mysql::params::purge_conf_dir, + $remove_default_accounts = false, + $restart = $mysql::params::restart, + $root_group = $mysql::params::root_group, + $root_password = $mysql::params::root_password, + $service_enabled = $mysql::params::server_service_enabled, + $service_manage = $mysql::params::server_service_manage, + $service_name = $mysql::params::server_service_name, + $service_provider = $mysql::params::server_service_provider, + $users = {}, + $grants = {}, + $databases = {}, + + # Deprecated parameters + $enabled = undef, + $manage_service = undef +) inherits mysql::params { + + # Deprecated parameters. + if $enabled { + crit('This parameter has been renamed to service_enabled.') + $real_service_enabled = $enabled + } else { + $real_service_enabled = $service_enabled + } + if $manage_service { + crit('This parameter has been renamed to service_manage.') + $real_service_manage = $manage_service + } else { + $real_service_manage = $service_manage + } + + # Create a merged together set of options. Rightmost hashes win over left. + $options = mysql_deepmerge($mysql::params::default_options, $override_options) + + Class['mysql::server::root_password'] -> Mysql::Db <| |> + + include '::mysql::server::install' + include '::mysql::server::config' + include '::mysql::server::service' + include '::mysql::server::root_password' + include '::mysql::server::providers' + + if $remove_default_accounts { + class { '::mysql::server::account_security': + require => Anchor['mysql::server::end'], + } + } + + anchor { 'mysql::server::start': } + anchor { 'mysql::server::end': } + + if $restart { + Anchor['mysql::server::start'] -> + Class['mysql::server::install'] -> + # Only difference between the blocks is that we use ~> to restart if + # restart is set to true. + Class['mysql::server::config'] ~> + Class['mysql::server::service'] -> + Class['mysql::server::root_password'] -> + Class['mysql::server::providers'] -> + Anchor['mysql::server::end'] + } else { + Anchor['mysql::server::start'] -> + Class['mysql::server::install'] -> + Class['mysql::server::config'] -> + Class['mysql::server::service'] -> + Class['mysql::server::root_password'] -> + Class['mysql::server::providers'] -> + Anchor['mysql::server::end'] + } + + +} diff --git a/mysql/manifests/server/account_security.pp b/mysql/manifests/server/account_security.pp new file mode 100644 index 000000000..36d943757 --- /dev/null +++ b/mysql/manifests/server/account_security.pp @@ -0,0 +1,22 @@ +class mysql::server::account_security { + mysql_user { + [ "root@${::fqdn}", + 'root@127.0.0.1', + 'root@::1', + "@${::fqdn}", + '@localhost', + '@%']: + ensure => 'absent', + require => Anchor['mysql::server::end'], + } + if ($::fqdn != $::hostname) { + mysql_user { ["root@${::hostname}", "@${::hostname}"]: + ensure => 'absent', + require => Anchor['mysql::server::end'], + } + } + mysql_database { 'test': + ensure => 'absent', + require => Anchor['mysql::server::end'], + } +} diff --git a/mysql/manifests/server/backup.pp b/mysql/manifests/server/backup.pp new file mode 100644 index 000000000..6e9ecdee9 --- /dev/null +++ b/mysql/manifests/server/backup.pp @@ -0,0 +1,62 @@ +# See README.me for usage. +class mysql::server::backup ( + $backupuser, + $backuppassword, + $backupdir, + $backupdirmode = '0700', + $backupdirowner = 'root', + $backupdirgroup = 'root', + $backupcompress = true, + $backuprotate = 30, + $ignore_events = true, + $delete_before_dump = false, + $backupdatabases = [], + $file_per_database = false, + $ensure = 'present', + $time = ['23', '5'], + $postscript = false, + $execpath = '/usr/bin:/usr/sbin:/bin:/sbin', +) { + + mysql_user { "${backupuser}@localhost": + ensure => $ensure, + password_hash => mysql_password($backuppassword), + provider => 'mysql', + require => Class['mysql::server::root_password'], + } + + mysql_grant { "${backupuser}@localhost/*.*": + ensure => $ensure, + user => "${backupuser}@localhost", + table => '*.*', + privileges => [ 'SELECT', 'RELOAD', 'LOCK TABLES', 'SHOW VIEW', 'PROCESS' ], + require => Mysql_user["${backupuser}@localhost"], + } + + cron { 'mysql-backup': + ensure => $ensure, + command => '/usr/local/sbin/mysqlbackup.sh', + user => 'root', + hour => $time[0], + minute => $time[1], + 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 => $backupdirmode, + owner => $backupdirowner, + group => $backupdirgroup, + } + +} diff --git a/mysql/manifests/server/config.pp b/mysql/manifests/server/config.pp new file mode 100644 index 000000000..6a7e8ea8f --- /dev/null +++ b/mysql/manifests/server/config.pp @@ -0,0 +1,35 @@ +# See README.me for options. +class mysql::server::config { + + $options = $mysql::server::options + $includedir = $mysql::server::includedir + + File { + owner => 'root', + group => $mysql::server::root_group, + mode => '0400', + } + + if $includedir and $includedir != '' { + file { "$mysql::server::includedir": + ensure => directory, + mode => '0755', + recurse => $mysql::server::purge_conf_dir, + purge => $mysql::server::purge_conf_dir, + } + } + + if $mysql::server::manage_config_file { + file { 'mysql-config-file': + path => $mysql::server::config_file, + content => template('mysql/my.cnf.erb'), + mode => '0644', + } + } + + if $options['mysqld']['ssl-disable'] { + notify {'ssl-disable': + message =>'Disabling SSL is evil! You should never ever do this except if you are forced to use a mysql version compiled without SSL support' + } + } +} diff --git a/mysql/manifests/server/install.pp b/mysql/manifests/server/install.pp new file mode 100644 index 000000000..f7736d586 --- /dev/null +++ b/mysql/manifests/server/install.pp @@ -0,0 +1,29 @@ +# +class mysql::server::install { + + package { 'mysql-server': + ensure => $mysql::server::package_ensure, + name => $mysql::server::package_name, + } + + # Build the initial databases. + if $mysql::server::override_options['mysqld'] and $mysql::server::override_options['mysqld']['datadir'] { + $mysqluser = $mysql::server::options['mysqld']['user'] + $datadir = $mysql::server::override_options['mysqld']['datadir'] + + exec { 'mysql_install_db': + command => "mysql_install_db --datadir=${datadir} --user=${mysqluser}", + creates => "${datadir}/mysql", + logoutput => on_failure, + path => '/bin:/sbin:/usr/bin:/usr/sbin', + require => Package['mysql-server'], + } + + if $mysql::server::restart { + Exec['mysql_install_db'] { + notify => Class['mysql::server::service'], + } + } + } + +} diff --git a/mysql/manifests/server/monitor.pp b/mysql/manifests/server/monitor.pp new file mode 100644 index 000000000..9e86b92c7 --- /dev/null +++ b/mysql/manifests/server/monitor.pp @@ -0,0 +1,24 @@ +#This is a helper class to add a monitoring user to the database +class mysql::server::monitor ( + $mysql_monitor_username, + $mysql_monitor_password, + $mysql_monitor_hostname +) { + + Anchor['mysql::server::end'] -> Class['mysql::server::monitor'] + + mysql_user { "${mysql_monitor_username}@${mysql_monitor_hostname}": + ensure => present, + password_hash => mysql_password($mysql_monitor_password), + require => Class['mysql::server::service'], + } + + mysql_grant { "${mysql_monitor_username}@${mysql_monitor_hostname}/*.*": + ensure => present, + user => "${mysql_monitor_username}@${mysql_monitor_hostname}", + table => '*.*', + privileges => [ 'PROCESS', 'SUPER' ], + require => Mysql_user["${mysql_monitor_username}@${mysql_monitor_hostname}"], + } + +} diff --git a/mysql/manifests/server/mysqltuner.pp b/mysql/manifests/server/mysqltuner.pp new file mode 100644 index 000000000..23d19a558 --- /dev/null +++ b/mysql/manifests/server/mysqltuner.pp @@ -0,0 +1,9 @@ +# +class mysql::server::mysqltuner($ensure='present') { + # mysql performance tester + file { '/usr/local/bin/mysqltuner': + ensure => $ensure, + mode => '0550', + source => 'puppet:///modules/mysql/mysqltuner.pl', + } +} diff --git a/mysql/manifests/server/providers.pp b/mysql/manifests/server/providers.pp new file mode 100644 index 000000000..b651172fc --- /dev/null +++ b/mysql/manifests/server/providers.pp @@ -0,0 +1,8 @@ +# Convenience class to call each of the three providers with the corresponding +# hashes provided in mysql::server. +# See README.md for details. +class mysql::server::providers { + create_resources('mysql_user', $mysql::server::users) + create_resources('mysql_grant', $mysql::server::grants) + create_resources('mysql_database', $mysql::server::databases) +} diff --git a/mysql/manifests/server/root_password.pp b/mysql/manifests/server/root_password.pp new file mode 100644 index 000000000..e75412dab --- /dev/null +++ b/mysql/manifests/server/root_password.pp @@ -0,0 +1,21 @@ +# +class mysql::server::root_password { + + $options = $mysql::server::options + + # manage root password if it is set + if $mysql::server::root_password != 'UNSET' { + mysql_user { 'root@localhost': + ensure => present, + password_hash => mysql_password($mysql::server::root_password), + } + + file { "${::root_home}/.my.cnf": + content => template('mysql/my.cnf.pass.erb'), + owner => 'root', + mode => '0600', + require => Mysql_user['root@localhost'], + } + } + +} diff --git a/mysql/manifests/server/service.pp b/mysql/manifests/server/service.pp new file mode 100644 index 000000000..7dab2b56b --- /dev/null +++ b/mysql/manifests/server/service.pp @@ -0,0 +1,23 @@ +# +class mysql::server::service { + + if $mysql::server::real_service_enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + + if $mysql::server::real_service_manage { + file { $mysql::params::log_error: + owner => 'mysql', + group => 'mysql', + } + service { 'mysqld': + ensure => $service_ensure, + name => $mysql::server::service_name, + enable => $mysql::server::real_service_enabled, + provider => $mysql::server::service_provider, + } + } + +} diff --git a/mysql/metadata.json b/mysql/metadata.json new file mode 100644 index 000000000..0e9cfe536 --- /dev/null +++ b/mysql/metadata.json @@ -0,0 +1,82 @@ +{ + "name": "puppetlabs-mysql", + "version": "2.3.1", + "author": "Puppet Labs", + "summary": "Mysql module", + "license": "Apache 2.0", + "source": "git://github.com/puppetlabs/puppetlabs-mysql.git", + "project_page": "http://github.com/puppetlabs/puppetlabs-mysql", + "issues_url": "https://github.com/puppetlabs/puppetlabs-mysql/issues", + "operatingsystem_support": [ + { + "operatingsystem": "RedHat", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "CentOS", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "OracleLinux", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "Scientific", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "SLES", + "operatingsystemrelease": [ + "11 SP1" + ] + }, + { + "operatingsystem": "Debian", + "operatingsystemrelease": [ + "6", + "7" + ] + }, + { + "operatingsystem": "Ubuntu", + "operatingsystemrelease": [ + "10.04", + "12.04", + "14.04" + ] + } + ], + "requirements": [ + { + "name": "pe", + "version_requirement": ">= 3.2.0 < 3.4.0" + }, + { + "name": "puppet", + "version_requirement": "3.x" + } + ], + "description": "Mysql module", + "dependencies": [ + { + "name": "puppetlabs/stdlib", + "version_requirement": ">= 3.2.0" + } + ] +} diff --git a/mysql/spec/acceptance/mysql_backup_spec.rb b/mysql/spec/acceptance/mysql_backup_spec.rb new file mode 100644 index 000000000..5b67c8b25 --- /dev/null +++ b/mysql/spec/acceptance/mysql_backup_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper_acceptance' + +describe 'mysql::server::backup class' do + context 'should work with no errors' do + it 'when configuring mysql backups' do + pp = <<-EOS + class { 'mysql::server': root_password => 'password' } + mysql::db { [ + 'backup1', + 'backup2' + ]: + user => 'backup', + password => 'secret', + } + + class { 'mysql::server::backup': + backupuser => 'myuser', + backuppassword => 'mypassword', + backupdir => '/tmp/backups', + backupcompress => true, + postscript => [ + 'rm -rf /var/tmp/mysqlbackups', + 'rm -f /var/tmp/mysqlbackups.done', + 'cp -r /tmp/backups /var/tmp/mysqlbackups', + 'touch /var/tmp/mysqlbackups.done', + ], + execpath => '/usr/bin:/usr/sbin:/bin:/sbin:/opt/zimbra/bin', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_failures => true) + end + end + + describe 'mysqlbackup.sh' do + it 'should run mysqlbackup.sh with no errors' do + shell("/usr/local/sbin/mysqlbackup.sh") do |r| + expect(r.stderr).to eq("") + end + end + + it 'should dump all databases to single file' do + shell('ls -l /tmp/backups/mysql_backup_*-*.sql.bz2 | wc -l') do |r| + expect(r.stdout).to match(/1/) + expect(r.exit_code).to be_zero + end + end + + context 'should create one file per database per run' do + it 'executes mysqlbackup.sh a second time' do + shell('sleep 1') + shell('/usr/local/sbin/mysqlbackup.sh') + end + + it 'creates at least one backup tarball' do + shell('ls -l /tmp/backups/mysql_backup_*-*.sql.bz2 | wc -l') do |r| + expect(r.stdout).to match(/2/) + expect(r.exit_code).to be_zero + end + end + end + end +end diff --git a/mysql/spec/acceptance/mysql_db_spec.rb b/mysql/spec/acceptance/mysql_db_spec.rb new file mode 100644 index 000000000..88ded3aec --- /dev/null +++ b/mysql/spec/acceptance/mysql_db_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper_acceptance' + +describe 'mysql::db define' do + describe 'creating a database' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'mysql::server': root_password => 'password' } + mysql::db { 'spec1': + user => 'root1', + password => 'password', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + + expect(shell("mysql -e 'show databases;'|grep spec1").exit_code).to be_zero + end + end + + describe 'creating a database with post-sql' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'mysql::server': override_options => { 'root_password' => 'password' } } + file { '/tmp/spec.sql': + ensure => file, + content => 'CREATE TABLE table1 (id int);', + before => Mysql::Db['spec2'], + } + mysql::db { 'spec2': + user => 'root1', + password => 'password', + sql => '/tmp/spec.sql', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should have the table' do + expect(shell("mysql -e 'show tables;' spec2|grep table1").exit_code).to be_zero + end + end + + describe 'creating a database with dbname parameter' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'mysql::server': override_options => { 'root_password' => 'password' } } + mysql::db { 'spec1': + user => 'root1', + password => 'password', + dbname => 'realdb', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should have the database named realdb' do + expect(shell("mysql -e 'show databases;'|grep realdb").exit_code).to be_zero + end + end +end diff --git a/mysql/spec/acceptance/mysql_server_spec.rb b/mysql/spec/acceptance/mysql_server_spec.rb new file mode 100644 index 000000000..06646cbbe --- /dev/null +++ b/mysql/spec/acceptance/mysql_server_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper_acceptance' + +describe 'mysql class' do + + describe 'running puppet code' do + # Using puppet_apply as a helper + it 'should work with no errors' do + tmpdir = default.tmpdir('mysql') + pp = <<-EOS + class { 'mysql::server': + config_file => '#{tmpdir}/my.cnf', + includedir => '#{tmpdir}/include', + manage_config_file => 'true', + override_options => { 'mysqld' => { 'key_buffer_size' => '32M' }}, + package_ensure => 'present', + purge_conf_dir => 'true', + remove_default_accounts => 'true', + restart => 'true', + root_group => 'root', + root_password => 'test', + service_enabled => 'true', + service_manage => 'true', + users => { + 'someuser@localhost' => { + ensure => 'present', + max_connections_per_hour => '0', + max_queries_per_hour => '0', + max_updates_per_hour => '0', + max_user_connections => '0', + password_hash => '*F3A2A51A9B0F2BE2468926B4132313728C250DBF', + }}, + grants => { + 'someuser@localhost/somedb.*' => { + ensure => 'present', + options => ['GRANT'], + privileges => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'], + table => 'somedb.*', + user => 'someuser@localhost', + }, + }, + databases => { + 'somedb' => { + ensure => 'present', + charset => 'utf8', + }, + } + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + end +end + diff --git a/mysql/spec/acceptance/nodesets/centos-510-x64.yml b/mysql/spec/acceptance/nodesets/centos-510-x64.yml new file mode 100644 index 000000000..12c9e7893 --- /dev/null +++ b/mysql/spec/acceptance/nodesets/centos-510-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-510-x64: + roles: + - master + platform: el-5-x86_64 + box : centos-510-x64-virtualbox-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-510-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/mysql/spec/acceptance/nodesets/centos-59-x64.yml b/mysql/spec/acceptance/nodesets/centos-59-x64.yml new file mode 100644 index 000000000..2ad90b86a --- /dev/null +++ b/mysql/spec/acceptance/nodesets/centos-59-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-59-x64: + roles: + - master + platform: el-5-x86_64 + box : centos-59-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-59-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/mysql/spec/acceptance/nodesets/centos-64-x64-pe.yml b/mysql/spec/acceptance/nodesets/centos-64-x64-pe.yml new file mode 100644 index 000000000..7d9242f1b --- /dev/null +++ b/mysql/spec/acceptance/nodesets/centos-64-x64-pe.yml @@ -0,0 +1,12 @@ +HOSTS: + centos-64-x64: + roles: + - master + - database + - dashboard + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: pe diff --git a/mysql/spec/acceptance/nodesets/centos-65-x64.yml b/mysql/spec/acceptance/nodesets/centos-65-x64.yml new file mode 100644 index 000000000..4e2cb809e --- /dev/null +++ b/mysql/spec/acceptance/nodesets/centos-65-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-65-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-65-x64-vbox436-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-65-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mysql/spec/acceptance/nodesets/default.yml b/mysql/spec/acceptance/nodesets/default.yml new file mode 100644 index 000000000..6505c6ded --- /dev/null +++ b/mysql/spec/acceptance/nodesets/default.yml @@ -0,0 +1,11 @@ +HOSTS: + centos-64-x64: + roles: + - master + - default + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mysql/spec/acceptance/nodesets/fedora-18-x64.yml b/mysql/spec/acceptance/nodesets/fedora-18-x64.yml new file mode 100644 index 000000000..136164983 --- /dev/null +++ b/mysql/spec/acceptance/nodesets/fedora-18-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + fedora-18-x64: + roles: + - master + platform: fedora-18-x86_64 + box : fedora-18-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/fedora-18-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mysql/spec/acceptance/nodesets/sles-11-x64.yml b/mysql/spec/acceptance/nodesets/sles-11-x64.yml new file mode 100644 index 000000000..41abe2135 --- /dev/null +++ b/mysql/spec/acceptance/nodesets/sles-11-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + sles-11-x64.local: + roles: + - master + platform: sles-11-x64 + box : sles-11sp1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/sles-11sp1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mysql/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml b/mysql/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml new file mode 100644 index 000000000..5ca1514e4 --- /dev/null +++ b/mysql/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-10044-x64: + roles: + - master + platform: ubuntu-10.04-amd64 + box : ubuntu-server-10044-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-10044-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mysql/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml b/mysql/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml new file mode 100644 index 000000000..d065b304f --- /dev/null +++ b/mysql/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-12042-x64: + roles: + - master + platform: ubuntu-12.04-amd64 + box : ubuntu-server-12042-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/mysql/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml b/mysql/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml new file mode 100644 index 000000000..cba1cd04c --- /dev/null +++ b/mysql/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + ubuntu-server-1404-x64: + roles: + - master + platform: ubuntu-14.04-amd64 + box : puppetlabs/ubuntu-14.04-64-nocm + box_url : https://vagrantcloud.com/puppetlabs/ubuntu-14.04-64-nocm + hypervisor : vagrant +CONFIG: + log_level : debug + type: git diff --git a/mysql/spec/acceptance/types/mysql_database_spec.rb b/mysql/spec/acceptance/types/mysql_database_spec.rb new file mode 100644 index 000000000..b19026ddb --- /dev/null +++ b/mysql/spec/acceptance/types/mysql_database_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper_acceptance' + +describe 'mysql_database' do + describe 'setup' do + it 'should work with no errors' do + pp = <<-EOS + class { 'mysql::server': } + EOS + + apply_manifest(pp, :catch_failures => true) + end + end + + describe 'creating database' do + it 'should work without errors' do + pp = <<-EOS + mysql_database { 'spec_db': + ensure => present, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should find the database' do + shell("mysql -NBe \"SHOW DATABASES LIKE 'spec_db'\"") do |r| + expect(r.stdout).to match(/^spec_db$/) + expect(r.stderr).to be_empty + end + end + end + + describe 'charset and collate' do + it 'should create two db of different types idempotently' do + pp = <<-EOS + mysql_database { 'spec_latin1': + charset => 'latin1', + collate => 'latin1_swedish_ci', + } + mysql_database { 'spec_utf8': + charset => 'utf8', + collate => 'utf8_general_ci', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + + it 'should find latin1 db' do + shell("mysql -NBe \"SHOW VARIABLES LIKE '%_database'\" spec_latin1") do |r| + expect(r.stdout).to match(/^character_set_database\tlatin1\ncollation_database\tlatin1_swedish_ci$/) + expect(r.stderr).to be_empty + end + end + + it 'should find utf8 db' do + shell("mysql -NBe \"SHOW VARIABLES LIKE '%_database'\" spec_utf8") do |r| + expect(r.stdout).to match(/^character_set_database\tutf8\ncollation_database\tutf8_general_ci$/) + expect(r.stderr).to be_empty + end + end + end +end diff --git a/mysql/spec/acceptance/types/mysql_grant_spec.rb b/mysql/spec/acceptance/types/mysql_grant_spec.rb new file mode 100644 index 000000000..e3474c533 --- /dev/null +++ b/mysql/spec/acceptance/types/mysql_grant_spec.rb @@ -0,0 +1,394 @@ +require 'spec_helper_acceptance' + +describe 'mysql_grant' do + + describe 'setup' do + it 'setup mysql::server' do + pp = <<-EOS + class { 'mysql::server': } + EOS + + apply_manifest(pp, :catch_failures => true) + end + end + + describe 'missing privileges for user' do + it 'should fail' do + pp = <<-EOS + mysql_grant { 'test1@tester/test.*': + ensure => 'present', + table => 'test.*', + user => 'test1@tester', + } + EOS + + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/privileges parameter is required/) + end + + it 'should not find the user' do + expect(shell("mysql -NBe \"SHOW GRANTS FOR test1@tester\"", { :acceptable_exit_codes => 1}).stderr).to match(/There is no such grant defined for user 'test1' on host 'tester'/) + end + end + + describe 'missing table for user' do + it 'should fail' do + pp = <<-EOS + mysql_grant { 'atest@tester/test.*': + ensure => 'present', + user => 'atest@tester', + privileges => ['ALL'], + } + EOS + + apply_manifest(pp, :expect_failures => true) + end + + it 'should not find the user' do + expect(shell("mysql -NBe \"SHOW GRANTS FOR atest@tester\"", {:acceptable_exit_codes => 1}).stderr).to match(/There is no such grant defined for user 'atest' on host 'tester'/) + end + end + + describe 'adding privileges' do + it 'should work without errors' do + pp = <<-EOS + mysql_grant { 'test2@tester/test.*': + ensure => 'present', + table => 'test.*', + user => 'test2@tester', + privileges => ['SELECT', 'UPDATE'], + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should find the user' do + shell("mysql -NBe \"SHOW GRANTS FOR test2@tester\"") do |r| + expect(r.stdout).to match(/GRANT SELECT, UPDATE.*TO 'test2'@'tester'/) + expect(r.stderr).to be_empty + end + end + end + + describe 'adding privileges with invalid name' do + it 'should fail' do + pp = <<-EOS + mysql_grant { 'test': + ensure => 'present', + table => 'test.*', + user => 'test2@tester', + privileges => ['SELECT', 'UPDATE'], + } + EOS + + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/name must match user and table parameters/) + end + end + + describe 'adding option' do + it 'should work without errors' do + pp = <<-EOS + mysql_grant { 'test3@tester/test.*': + ensure => 'present', + table => 'test.*', + user => 'test3@tester', + options => ['GRANT'], + privileges => ['SELECT', 'UPDATE'], + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should find the user' do + shell("mysql -NBe \"SHOW GRANTS FOR test3@tester\"") do |r| + expect(r.stdout).to match(/GRANT SELECT, UPDATE ON `test`.* TO 'test3'@'tester' WITH GRANT OPTION$/) + expect(r.stderr).to be_empty + end + end + end + + describe 'adding all privileges without table' do + it 'should fail' do + pp = <<-EOS + mysql_grant { 'test4@tester/test.*': + ensure => 'present', + user => 'test4@tester', + options => ['GRANT'], + privileges => ['SELECT', 'UPDATE', 'ALL'], + } + EOS + + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/table parameter is required./) + end + end + + describe 'adding all privileges' do + it 'should only try to apply ALL' do + pp = <<-EOS + mysql_grant { 'test4@tester/test.*': + ensure => 'present', + table => 'test.*', + user => 'test4@tester', + options => ['GRANT'], + privileges => ['SELECT', 'UPDATE', 'ALL'], + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should find the user' do + shell("mysql -NBe \"SHOW GRANTS FOR test4@tester\"") do |r| + expect(r.stdout).to match(/GRANT ALL PRIVILEGES ON `test`.* TO 'test4'@'tester' WITH GRANT OPTION/) + expect(r.stderr).to be_empty + end + end + end + + # Test combinations of user@host to ensure all cases work. + describe 'short hostname' do + it 'should apply' do + pp = <<-EOS + mysql_grant { 'test@short/test.*': + ensure => 'present', + table => 'test.*', + user => 'test@short', + privileges => 'ALL', + } + mysql_grant { 'test@long.hostname.com/test.*': + ensure => 'present', + table => 'test.*', + user => 'test@long.hostname.com', + privileges => 'ALL', + } + mysql_grant { 'test@192.168.5.6/test.*': + ensure => 'present', + table => 'test.*', + user => 'test@192.168.5.6', + privileges => 'ALL', + } + mysql_grant { 'test@2607:f0d0:1002:0051:0000:0000:0000:0004/test.*': + ensure => 'present', + table => 'test.*', + user => 'test@2607:f0d0:1002:0051:0000:0000:0000:0004', + privileges => 'ALL', + } + mysql_grant { 'test@::1/128/test.*': + ensure => 'present', + table => 'test.*', + user => 'test@::1/128', + privileges => 'ALL', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'finds short hostname' do + shell("mysql -NBe \"SHOW GRANTS FOR test@short\"") do |r| + expect(r.stdout).to match(/GRANT ALL PRIVILEGES ON `test`.* TO 'test'@'short'/) + expect(r.stderr).to be_empty + end + end + it 'finds long hostname' do + shell("mysql -NBe \"SHOW GRANTS FOR 'test'@'long.hostname.com'\"") do |r| + expect(r.stdout).to match(/GRANT ALL PRIVILEGES ON `test`.* TO 'test'@'long.hostname.com'/) + expect(r.stderr).to be_empty + end + end + it 'finds ipv4' do + shell("mysql -NBe \"SHOW GRANTS FOR 'test'@'192.168.5.6'\"") do |r| + expect(r.stdout).to match(/GRANT ALL PRIVILEGES ON `test`.* TO 'test'@'192.168.5.6'/) + expect(r.stderr).to be_empty + end + end + it 'finds ipv6' do + shell("mysql -NBe \"SHOW GRANTS FOR 'test'@'2607:f0d0:1002:0051:0000:0000:0000:0004'\"") do |r| + expect(r.stdout).to match(/GRANT ALL PRIVILEGES ON `test`.* TO 'test'@'2607:f0d0:1002:0051:0000:0000:0000:0004'/) + expect(r.stderr).to be_empty + end + end + it 'finds short ipv6' do + shell("mysql -NBe \"SHOW GRANTS FOR 'test'@'::1/128'\"") do |r| + expect(r.stdout).to match(/GRANT ALL PRIVILEGES ON `test`.* TO 'test'@'::1\/128'/) + expect(r.stderr).to be_empty + end + end + end + + describe 'complex test' do + it 'setup mysql::server' do + pp = <<-EOS + $dbSubnet = '10.10.10.%' + + mysql_database { 'foo': + ensure => present, + } + + exec { 'mysql-create-table': + command => '/usr/bin/mysql -NBe "CREATE TABLE foo.bar (name VARCHAR(20))"', + environment => "HOME=${::root_home}", + unless => '/usr/bin/mysql -NBe "SELECT 1 FROM foo.bar LIMIT 1;"', + require => Mysql_database['foo'], + } + + Mysql_grant { + ensure => present, + options => ['GRANT'], + privileges => ['ALL'], + table => '*.*', + require => [ Mysql_database['foo'], Exec['mysql-create-table'] ], + } + + mysql_grant { "user1@${dbSubnet}/*.*": + user => "user1@${dbSubnet}", + } + mysql_grant { "user2@${dbSubnet}/foo.bar": + privileges => ['SELECT', 'INSERT', 'UPDATE'], + user => "user2@${dbSubnet}", + table => 'foo.bar', + } + mysql_grant { "user3@${dbSubnet}/foo.*": + privileges => ['SELECT', 'INSERT', 'UPDATE'], + user => "user3@${dbSubnet}", + table => 'foo.*', + } + mysql_grant { 'web@%/*.*': + user => 'web@%', + } + mysql_grant { "web@${dbSubnet}/*.*": + user => "web@${dbSubnet}", + } + mysql_grant { "web@${fqdn}/*.*": + user => "web@${fqdn}", + } + mysql_grant { 'web@localhost/*.*': + user => 'web@localhost', + } + EOS + + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + end + + describe 'lower case privileges' do + it 'create ALL privs' do + pp = <<-EOS + mysql_grant { 'lowercase@localhost/*.*': + user => 'lowercase@localhost', + privileges => 'ALL', + table => '*.*', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'create lowercase all privs' do + pp = <<-EOS + mysql_grant { 'lowercase@localhost/*.*': + user => 'lowercase@localhost', + privileges => 'all', + table => '*.*', + } + EOS + + expect(apply_manifest(pp, :catch_failures => true).exit_code).to eq(0) + end + end + + describe 'adding procedure privileges' do + it 'should work without errors' do + pp = <<-EOS + mysql_grant { 'test2@tester/PROCEDURE test.simpleproc': + ensure => 'present', + table => 'PROCEDURE test.simpleproc', + user => 'test2@tester', + privileges => ['EXECUTE'], + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should find the user' do + shell("mysql -NBe \"SHOW GRANTS FOR test2@tester\"") do |r| + expect(r.stdout).to match(/GRANT EXECUTE ON PROCEDURE `test`.`simpleproc` TO 'test2'@'tester'/) + expect(r.stderr).to be_empty + end + end + end + + describe 'grants with skip-name-resolve specified' do + it 'setup mysql::server' do + pp = <<-EOS + class { 'mysql::server': + override_options => { + 'mysqld' => {'skip-name-resolve' => true} + }, + restart => true, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should apply' do + pp = <<-EOS + mysql_grant { 'test@fqdn.com/test.*': + ensure => 'present', + table => 'test.*', + user => 'test@fqdn.com', + privileges => 'ALL', + } + mysql_grant { 'test@192.168.5.7/test.*': + ensure => 'present', + table => 'test.*', + user => 'test@192.168.5.7', + privileges => 'ALL', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should fail with fqdn' do + expect(shell("mysql -NBe \"SHOW GRANTS FOR test@fqdn.com\"", { :acceptable_exit_codes => 1}).stderr).to match(/There is no such grant defined for user 'test' on host 'fqdn.com'/) + end + it 'finds ipv4' do + shell("mysql -NBe \"SHOW GRANTS FOR 'test'@'192.168.5.7'\"") do |r| + expect(r.stdout).to match(/GRANT ALL PRIVILEGES ON `test`.* TO 'test'@'192.168.5.7'/) + expect(r.stderr).to be_empty + end + end + + it 'should fail to execute while applying' do + pp = <<-EOS + mysql_grant { 'test@fqdn.com/test.*': + ensure => 'present', + table => 'test.*', + user => 'test@fqdn.com', + privileges => 'ALL', + } + EOS + + mysql_cmd = shell('which mysql').stdout.chomp + shell("mv #{mysql_cmd} #{mysql_cmd}.bak") + expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/Command mysql is missing/) + shell("mv #{mysql_cmd}.bak #{mysql_cmd}") + end + + it 'reset mysql::server config' do + pp = <<-EOS + class { 'mysql::server': + restart => true, + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + end + +end diff --git a/mysql/spec/acceptance/types/mysql_user_spec.rb b/mysql/spec/acceptance/types/mysql_user_spec.rb new file mode 100644 index 000000000..a0fa4f78b --- /dev/null +++ b/mysql/spec/acceptance/types/mysql_user_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper_acceptance' + +describe 'mysql_user' do + describe 'setup' do + it 'should work with no errors' do + pp = <<-EOS + class { 'mysql::server': } + EOS + + apply_manifest(pp, :catch_failures => true) + end + end + + context 'using ashp@localhost' do + describe 'adding user' do + it 'should work without errors' do + pp = <<-EOS + mysql_user { 'ashp@localhost': + password_hash => '6f8c114b58f2ce9e', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should find the user' do + shell("mysql -NBe \"select '1' from mysql.user where CONCAT(user, '@', host) = 'ashp@localhost'\"") do |r| + expect(r.stdout).to match(/^1$/) + expect(r.stderr).to be_empty + end + end + end + end + + context 'using ashp@LocalHost' do + describe 'adding user' do + it 'should work without errors' do + pp = <<-EOS + mysql_user { 'ashp@LocalHost': + password_hash => '6f8c114b58f2ce9e', + } + EOS + + apply_manifest(pp, :catch_failures => true) + end + + it 'should find the user' do + shell("mysql -NBe \"select '1' from mysql.user where CONCAT(user, '@', host) = 'ashp@localhost'\"") do |r| + expect(r.stdout).to match(/^1$/) + expect(r.stderr).to be_empty + end + end + end + end +end diff --git a/mysql/spec/classes/graceful_failures_spec.rb b/mysql/spec/classes/graceful_failures_spec.rb new file mode 100644 index 000000000..d8f22b76e --- /dev/null +++ b/mysql/spec/classes/graceful_failures_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'mysql::server' do + on_pe_unsupported_platforms.each do |pe_version,pe_platforms| + pe_platforms.each do |pe_platform,facts| + describe "on #{pe_version} #{pe_platform}" do + let(:facts) { facts } + + context 'should gracefully fail' do + it { expect { should compile}.to raise_error(Puppet::Error, /Unsupported platform:/) } + end + end + end + end +end diff --git a/mysql/spec/classes/mysql_bindings_spec.rb b/mysql/spec/classes/mysql_bindings_spec.rb new file mode 100644 index 000000000..0ee2e89b2 --- /dev/null +++ b/mysql/spec/classes/mysql_bindings_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe 'mysql::bindings' do + let(:params) {{ + 'java_enable' => true, + 'perl_enable' => true, + 'php_enable' => true, + 'python_enable' => true, + 'ruby_enable' => true, + 'client_dev' => true, + 'daemon_dev' => true, + }} + + shared_examples 'bindings' do |osfamily, operatingsystem, operatingsystemrelease, java_name, perl_name, php_name, python_name, ruby_name, client_dev_name, daemon_dev_name| + let :facts do + { :osfamily => osfamily, :operatingsystem => operatingsystem, + :operatingsystemrelease => operatingsystemrelease, :root_home => '/root', + } + end + it { should contain_package('mysql-connector-java').with( + :name => java_name, + :ensure => 'present' + )} + it { should contain_package('perl_mysql').with( + :name => perl_name, + :ensure => 'present' + )} + it { should contain_package('python-mysqldb').with( + :name => python_name, + :ensure => 'present' + )} + it { should contain_package('ruby_mysql').with( + :name => ruby_name, + :ensure => 'present' + )} + if client_dev_name + it { should contain_package('mysql-client_dev').with( + :name => client_dev_name, + :ensure => 'present' + )} + end + if daemon_dev_name + it { should contain_package('mysql-daemon_dev').with( + :name => daemon_dev_name, + :ensure => 'present' + )} + end + end + + context 'Debian' do + it_behaves_like 'bindings', 'Debian', 'Debian', '7.4','libmysql-java', 'libdbd-mysql-perl', 'php5-mysql', 'python-mysqldb', 'libmysql-ruby', 'libmysqlclient-dev', 'libmysqld-dev' + it_behaves_like 'bindings', 'Debian', 'Ubuntu', '14.04', 'libmysql-java', 'libdbd-mysql-perl', 'php5-mysql', 'python-mysqldb', 'libmysql-ruby', 'libmysqlclient-dev', 'libmysqld-dev' + end + + context 'freebsd' do + it_behaves_like 'bindings', 'FreeBSD', 'FreeBSD', '10.0', 'databases/mysql-connector-java', 'p5-DBD-mysql', 'databases/php5-mysql', 'databases/py-MySQLdb', 'databases/ruby-mysql' + end + + context 'redhat' do + it_behaves_like 'bindings', 'RedHat', 'RedHat', '6.5', 'mysql-connector-java', 'perl-DBD-MySQL', 'php-mysql', 'MySQL-python', 'ruby-mysql', nil, 'mysql-devel' + it_behaves_like 'bindings', 'RedHat', 'OpenSuSE', '11.3', 'mysql-connector-java', 'perl-DBD-MySQL', 'php-mysql', 'MySQL-python', 'ruby-mysql', nil, 'mysql-devel' + end + + describe 'on any other os' do + let :facts do + {:osfamily => 'foo', :root_home => '/root'} + end + + it 'should fail' do + expect { subject }.to raise_error(/Unsupported platform:/) + end + end + +end diff --git a/mysql/spec/classes/mysql_client_spec.rb b/mysql/spec/classes/mysql_client_spec.rb new file mode 100644 index 000000000..d5e63e27e --- /dev/null +++ b/mysql/spec/classes/mysql_client_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'mysql::client' do + on_pe_supported_platforms(PLATFORMS).each do |pe_version,pe_platforms| + pe_platforms.each do |pe_platform,facts| + describe "on #{pe_version} #{pe_platform}" do + let(:facts) { facts } + + context 'with defaults' do + it { should_not contain_class('mysql::bindings') } + it { should contain_package('mysql_client') } + end + + context 'with bindings enabled' do + let(:params) {{ :bindings_enable => true }} + + it { should contain_class('mysql::bindings') } + it { should contain_package('mysql_client') } + end + end + end + end +end diff --git a/mysql/spec/classes/mysql_server_account_security_spec.rb b/mysql/spec/classes/mysql_server_account_security_spec.rb new file mode 100644 index 000000000..855856fce --- /dev/null +++ b/mysql/spec/classes/mysql_server_account_security_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'mysql::server::account_security' do + on_pe_supported_platforms(PLATFORMS).each do |pe_version,pe_platforms| + pe_platforms.each do |pe_platform,facts| + describe "on #{pe_version} #{pe_platform}" do + let(:facts) { facts.merge({:fqdn => 'myhost.mydomain', :hostname => 'myhost'}) } + + it 'should remove Mysql_User[root@myhost.mydomain]' do + should contain_mysql_user('root@myhost.mydomain').with_ensure('absent') + end + it 'should remove Mysql_User[root@myhost]' do + should contain_mysql_user('root@myhost').with_ensure('absent') + end + it 'should remove Mysql_User[root@127.0.0.1]' do + should contain_mysql_user('root@127.0.0.1').with_ensure('absent') + end + it 'should remove Mysql_User[root@::1]' do + should contain_mysql_user('root@::1').with_ensure('absent') + end + it 'should remove Mysql_User[@myhost.mydomain]' do + should contain_mysql_user('@myhost.mydomain').with_ensure('absent') + end + it 'should remove Mysql_User[@myhost]' do + should contain_mysql_user('@myhost').with_ensure('absent') + end + it 'should remove Mysql_User[@localhost]' do + should contain_mysql_user('@localhost').with_ensure('absent') + end + it 'should remove Mysql_User[@%]' do + should contain_mysql_user('@%').with_ensure('absent') + end + + it 'should remove Mysql_database[test]' do + should contain_mysql_database('test').with_ensure('absent') + end + end + end + end +end diff --git a/mysql/spec/classes/mysql_server_backup_spec.rb b/mysql/spec/classes/mysql_server_backup_spec.rb new file mode 100644 index 000000000..d244756d4 --- /dev/null +++ b/mysql/spec/classes/mysql_server_backup_spec.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +describe 'mysql::server::backup' do + on_pe_supported_platforms(PLATFORMS).each do |pe_version,pe_platforms| + pe_platforms.each do |pe_platform,facts| + describe "on #{pe_version} #{pe_platform}" do + let(:facts) { facts } + + let(:default_params) { + { 'backupuser' => 'testuser', + 'backuppassword' => 'testpass', + 'backupdir' => '/tmp', + 'backuprotate' => '25', + 'delete_before_dump' => true, + 'execpath' => '/usr/bin:/usr/sbin:/bin:/sbin:/opt/zimbra/bin', + } + } + + context 'standard conditions' do + let(:params) { default_params } + + it { should contain_mysql_user('testuser@localhost').with( + :require => 'Class[Mysql::Server::Root_password]' + )} + + it { should contain_mysql_grant('testuser@localhost/*.*').with( + :privileges => ["SELECT", "RELOAD", "LOCK TABLES", "SHOW VIEW", "PROCESS"] + )} + + 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' + )} + + it 'should have compression by default' do + verify_contents(subject, 'mysqlbackup.sh', [ + ' --all-databases | bzcat -zc > ${DIR}/${PREFIX}`date +%Y%m%d-%H%M%S`.sql.bz2', + ]) + end + it 'should skip backing up events table by default' do + verify_contents(subject, 'mysqlbackup.sh', [ + 'EVENTS="--ignore-table=mysql.event"', + ]) + end + + it 'should have 25 days of rotation' do + # MySQL counts from 0 I guess. + should contain_file('mysqlbackup.sh').with_content(/.*ROTATE=24.*/) + end + + it 'should have a standard PATH' do + should contain_file('mysqlbackup.sh').with_content(%r{PATH=/usr/bin:/usr/sbin:/bin:/sbin:/opt/zimbra/bin}) + end + end + + context 'custom ownership and mode for backupdir' do + let(:params) do + { :backupdirmode => '0750', + :backupdirowner => 'testuser', + :backupdirgroup => 'testgrp', + }.merge(default_params) + end + + it { should contain_file('mysqlbackupdir').with( + :path => '/tmp', + :ensure => 'directory', + :mode => '0750', + :owner => 'testuser', + :group => 'testgrp' + ) } + end + + context 'with compression disabled' do + let(:params) do + { :backupcompress => false }.merge(default_params) + end + + it { should contain_file('mysqlbackup.sh').with( + :path => '/usr/local/sbin/mysqlbackup.sh', + :ensure => 'present' + ) } + + it 'should be able to disable compression' do + verify_contents(subject, 'mysqlbackup.sh', [ + ' --all-databases > ${DIR}/${PREFIX}`date +%Y%m%d-%H%M%S`.sql', + ]) + end + end + + context 'with mysql.events backedup' do + let(:params) do + { :ignore_events => false }.merge(default_params) + end + + it { should contain_file('mysqlbackup.sh').with( + :path => '/usr/local/sbin/mysqlbackup.sh', + :ensure => 'present' + ) } + + it 'should be able to backup events table' do + verify_contents(subject, 'mysqlbackup.sh', [ + 'EVENTS="--events"', + ]) + end + end + + + context 'with database list specified' do + let(:params) do + { :backupdatabases => ['mysql'] }.merge(default_params) + end + + it { should contain_file('mysqlbackup.sh').with( + :path => '/usr/local/sbin/mysqlbackup.sh', + :ensure => 'present' + ) } + + it 'should have a backup file for each database' do + content = subject.resource('file','mysqlbackup.sh').send(:parameters)[:content] + content.should match(' mysql | bzcat -zc \${DIR}\\\${PREFIX}mysql_`date') + # verify_contents(subject, 'mysqlbackup.sh', [ + # ' mysql | bzcat -zc ${DIR}/${PREFIX}mysql_`date +%Y%m%d-%H%M%S`.sql', + # ]) + end + end + + context 'with file per database' do + let(:params) do + default_params.merge({ :file_per_database => true }) + end + + it 'should loop through backup all databases' do + verify_contents(subject, 'mysqlbackup.sh', [ + 'mysql -s -r -N -e \'SHOW DATABASES\' | while read dbname', + 'do', + ' mysqldump -u${USER} -p${PASS} --opt --flush-logs --single-transaction \\', + ' ${EVENTS} \\', + ' ${dbname} | bzcat -zc > ${DIR}/${PREFIX}${dbname}_`date +%Y%m%d-%H%M%S`.sql.bz2', + 'done', + ]) + end + + context 'with compression disabled' do + let(:params) do + default_params.merge({ :file_per_database => true, :backupcompress => false }) + end + + it 'should loop through backup all databases without compression' do + verify_contents(subject, 'mysqlbackup.sh', [ + ' ${dbname} > ${DIR}/${PREFIX}${dbname}_`date +%Y%m%d-%H%M%S`.sql', + ]) + end + end + end + + context 'with postscript' do + let(:params) do + default_params.merge({ :postscript => 'rsync -a /tmp backup01.local-lan:' }) + end + + it 'should be add postscript' do + verify_contents(subject, 'mysqlbackup.sh', [ + 'rsync -a /tmp backup01.local-lan:', + ]) + end + end + + context 'with postscripts' do + let(:params) do + default_params.merge({ :postscript => [ + 'rsync -a /tmp backup01.local-lan:', + 'rsync -a /tmp backup02.local-lan:', + ]}) + end + + it 'should be add postscript' do + verify_contents(subject, 'mysqlbackup.sh', [ + 'rsync -a /tmp backup01.local-lan:', + 'rsync -a /tmp backup02.local-lan:', + ]) + end + end + end + end + end +end diff --git a/mysql/spec/classes/mysql_server_monitor_spec.rb b/mysql/spec/classes/mysql_server_monitor_spec.rb new file mode 100644 index 000000000..79ad33dd6 --- /dev/null +++ b/mysql/spec/classes/mysql_server_monitor_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +describe 'mysql::server::monitor' do + on_pe_supported_platforms(PLATFORMS).each do |pe_version,pe_platforms| + pe_platforms.each do |pe_platform,facts| + describe "on #{pe_version} #{pe_platform}" do + let(:facts) { facts } + let :pre_condition do + "include 'mysql::server'" + end + + let :default_params do + { + :mysql_monitor_username => 'monitoruser', + :mysql_monitor_password => 'monitorpass', + :mysql_monitor_hostname => 'monitorhost', + } + end + + let :params do + default_params + end + + it { should contain_mysql_user('monitoruser@monitorhost')} + + it { should contain_mysql_grant('monitoruser@monitorhost/*.*').with( + :ensure => 'present', + :user => 'monitoruser@monitorhost', + :table => '*.*', + :privileges => ["PROCESS", "SUPER"], + :require => 'Mysql_user[monitoruser@monitorhost]' + )} + end + end + end +end diff --git a/mysql/spec/classes/mysql_server_mysqltuner_spec.rb b/mysql/spec/classes/mysql_server_mysqltuner_spec.rb new file mode 100644 index 000000000..7645983d3 --- /dev/null +++ b/mysql/spec/classes/mysql_server_mysqltuner_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe 'mysql::server::mysqltuner' do + on_pe_supported_platforms(PLATFORMS).each do |pe_version,pe_platforms| + pe_platforms.each do |pe_platform,facts| + describe "on #{pe_version} #{pe_platform}" do + let(:facts) { facts } + + it { should contain_file('/usr/local/bin/mysqltuner') } + end + end + end +end diff --git a/mysql/spec/classes/mysql_server_spec.rb b/mysql/spec/classes/mysql_server_spec.rb new file mode 100644 index 000000000..b79e9f1ed --- /dev/null +++ b/mysql/spec/classes/mysql_server_spec.rb @@ -0,0 +1,239 @@ +require 'spec_helper' + +describe 'mysql::server' do + on_pe_supported_platforms(PLATFORMS).each do |pe_version,pe_platforms| + pe_platforms.each do |pe_platform,facts| + describe "on #{pe_version} #{pe_platform}" do + let(:facts) { facts } + + context 'with defaults' do + it { should contain_class('mysql::server::install') } + it { should contain_class('mysql::server::config') } + it { should contain_class('mysql::server::service') } + it { should contain_class('mysql::server::root_password') } + it { should contain_class('mysql::server::providers') } + end + + # make sure that overriding the mysqld settings keeps the defaults for everything else + context 'with overrides' do + let(:params) {{ :override_options => { 'mysqld' => { 'socket' => '/var/lib/mysql/mysql.sock' } } }} + it do + should contain_file('mysql-config-file').with({ + :mode => '0644', + }).with_content(/basedir/) + end + end + + describe 'with multiple instance of an option' do + let(:params) {{ :override_options => { 'mysqld' => { 'replicate-do-db' => ['base1', 'base2', 'base3'], } }}} + it do + should contain_file('mysql-config-file').with_content( + /^replicate-do-db = base1$/ + ).with_content( + /^replicate-do-db = base2$/ + ).with_content( + /^replicate-do-db = base3$/ + ) + end + end + + describe 'an option set to true' do + let(:params) { + { :override_options => { 'mysqld' => { 'ssl' => true } }} + } + it do + should contain_file('mysql-config-file').with_content(/^\s*ssl\s*(?:$|= true)/m) + end + end + + describe 'an option set to false' do + let(:params) { + { :override_options => { 'mysqld' => { 'ssl' => false } }} + } + it do + should contain_file('mysql-config-file').with_content(/^\s*ssl = false/m) + end + end + + context 'with remove_default_accounts set' do + let (:params) {{ :remove_default_accounts => true }} + it { should contain_class('mysql::server::account_security') } + end + + describe 'possibility of disabling ssl completely' do + let(:params) { + { :override_options => { 'mysqld' => { 'ssl' => true, 'ssl-disable' => true } }} + } + it do + should contain_file('mysql-config-file').without_content(/^\s*ssl\s*(?:$|= true)/m) + end + end + + context 'mysql::server::install' do + let(:params) {{ :package_ensure => 'present', :name => 'mysql-server' }} + it do + should contain_package('mysql-server').with({ + :ensure => :present, + :name => 'mysql-server', + }) + end + end + + if pe_platform =~ /redhat-7/ + context 'mysql::server::install on RHEL 7' do + let(:params) {{ :package_ensure => 'present', :name => 'mariadb-server' }} + it do + should contain_package('mysql-server').with({ + :ensure => :present, + :name => 'mariadb-server', + }) + end + end + end + + context 'mysql::server::config' do + context 'with includedir' do + let(:params) {{ :includedir => '/etc/my.cnf.d' }} + it do + should contain_file('/etc/my.cnf.d').with({ + :ensure => :directory, + :mode => '0755', + }) + end + + it do + should contain_file('mysql-config-file').with({ + :mode => '0644', + }) + end + + it do + should contain_file('mysql-config-file').with_content(/!includedir/) + end + end + + context 'without includedir' do + let(:params) {{ :includedir => '' }} + it do + should_not contain_file('mysql-config-file').with({ + :ensure => :directory, + :mode => '0755', + }) + end + + it do + should contain_file('mysql-config-file').with({ + :mode => '0644', + }) + end + + it do + should contain_file('mysql-config-file').without_content(/!includedir/) + end + end + end + + context 'mysql::server::service' do + context 'with defaults' do + it { should contain_service('mysqld') } + end + + context 'service_enabled set to false' do + let(:params) {{ :service_enabled => false }} + + it do + should contain_service('mysqld').with({ + :ensure => :stopped + }) + end + end + end + + context 'mysql::server::root_password' do + describe 'when defaults' do + it { should_not contain_mysql_user('root@localhost') } + it { should_not contain_file('/root/.my.cnf') } + end + describe 'when set' do + let(:params) {{:root_password => 'SET' }} + it { should contain_mysql_user('root@localhost') } + it { should contain_file('/root/.my.cnf') } + end + + end + + context 'mysql::server::providers' do + describe 'with users' do + let(:params) {{:users => { + 'foo@localhost' => { + 'max_connections_per_hour' => '1', + 'max_queries_per_hour' => '2', + 'max_updates_per_hour' => '3', + 'max_user_connections' => '4', + 'password_hash' => '*F3A2A51A9B0F2BE2468926B4132313728C250DBF' + }, + 'foo2@localhost' => {} + }}} + it { should contain_mysql_user('foo@localhost').with( + :max_connections_per_hour => '1', + :max_queries_per_hour => '2', + :max_updates_per_hour => '3', + :max_user_connections => '4', + :password_hash => '*F3A2A51A9B0F2BE2468926B4132313728C250DBF' + )} + it { should contain_mysql_user('foo2@localhost').with( + :max_connections_per_hour => nil, + :max_queries_per_hour => nil, + :max_updates_per_hour => nil, + :max_user_connections => nil, + :password_hash => '' + )} + end + + describe 'with grants' do + let(:params) {{:grants => { + 'foo@localhost/somedb.*' => { + 'user' => 'foo@localhost', + 'table' => 'somedb.*', + 'privileges' => ["SELECT", "UPDATE"], + 'options' => ["GRANT"], + }, + 'foo2@localhost/*.*' => { + 'user' => 'foo2@localhost', + 'table' => '*.*', + 'privileges' => ["SELECT"], + }, + }}} + it { should contain_mysql_grant('foo@localhost/somedb.*').with( + :user => 'foo@localhost', + :table => 'somedb.*', + :privileges => ["SELECT", "UPDATE"], + :options => ["GRANT"] + )} + it { should contain_mysql_grant('foo2@localhost/*.*').with( + :user => 'foo2@localhost', + :table => '*.*', + :privileges => ["SELECT"], + :options => nil + )} + end + + describe 'with databases' do + let(:params) {{:databases => { + 'somedb' => { + 'charset' => 'latin1', + 'collate' => 'latin1', + }, + 'somedb2' => {} + }}} + it { should contain_mysql_database('somedb').with( + :charset => 'latin1', + :collate => 'latin1' + )} + it { should contain_mysql_database('somedb2')} + end + end + end + end + end +end diff --git a/mysql/spec/defines/mysql_db_spec.rb b/mysql/spec/defines/mysql_db_spec.rb new file mode 100644 index 000000000..01ec80733 --- /dev/null +++ b/mysql/spec/defines/mysql_db_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe 'mysql::db', :type => :define do + let(:facts) {{ :osfamily => 'RedHat' }} + let(:title) { 'test_db' } + + let(:params) { + { 'user' => 'testuser', + 'password' => 'testpass', + } + } + + it 'should report an error when ensure is not present or absent' do + params.merge!({'ensure' => 'invalid_val'}) + expect { subject }.to raise_error(Puppet::Error, + /invalid_val is not supported for ensure\. Allowed values are 'present' and 'absent'\./) + end + + it 'should not notify the import sql exec if no sql script was provided' do + should contain_mysql_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('Mysql_database[test_db]') + end + + it 'should only import sql script on creation if not enforcing' do + params.merge!({'sql' => 'test_sql', 'enforce_sql' => false}) + should contain_exec('test_db-import').with_refreshonly(true) + end + + it 'should import sql script on creation if enforcing' do + params.merge!({'sql' => 'test_sql', 'enforce_sql' => true}) + should contain_exec('test_db-import').with_refreshonly(false) + end + + it 'should not create database and database user' do + params.merge!({'ensure' => 'absent', 'host' => 'localhost'}) + should contain_mysql_database('test_db').with_ensure('absent') + should contain_mysql_user('testuser@localhost').with_ensure('absent') + end + + it 'should create with an appropriate collate and charset' do + params.merge!({'charset' => 'utf8', 'collate' => 'utf8_danish_ci'}) + should contain_mysql_database('test_db').with({ + 'charset' => 'utf8', + 'collate' => 'utf8_danish_ci', + }) + end + + it 'should use dbname parameter as database name instead of name' do + params.merge!({'dbname' => 'real_db'}) + should contain_mysql_database('real_db') + end +end diff --git a/mysql/spec/spec.opts b/mysql/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/mysql/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/mysql/spec/spec_helper.rb b/mysql/spec/spec_helper.rb new file mode 100644 index 000000000..f3a260acb --- /dev/null +++ b/mysql/spec/spec_helper.rb @@ -0,0 +1,10 @@ +require 'puppetlabs_spec_helper/module_spec_helper' +require 'puppet_facts' +include PuppetFacts +RSpec.configure do |c| + c.formatter = :documentation +end + +# The default set of platforms to test again. +ENV['UNIT_TEST_PLATFORMS'] = 'centos-6-x86_64 ubuntu-1404-x86_64' +PLATFORMS = ENV['UNIT_TEST_PLATFORMS'].split(' ') diff --git a/mysql/spec/spec_helper_acceptance.rb b/mysql/spec/spec_helper_acceptance.rb new file mode 100644 index 000000000..28bcdd60c --- /dev/null +++ b/mysql/spec/spec_helper_acceptance.rb @@ -0,0 +1,39 @@ +require 'beaker-rspec' + +UNSUPPORTED_PLATFORMS = [ 'Windows', 'Solaris', 'AIX' ] + +unless ENV['RS_PROVISION'] == 'no' or ENV['BEAKER_provision'] == 'no' + if hosts.first.is_pe? + install_pe + else + install_puppet + end + hosts.each do |host| + on hosts, "mkdir -p #{host['distmoduledir']}" + end +end + +RSpec.configure do |c| + # Project root + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Readable test descriptions + c.formatter = :documentation + + # Configure all nodes in nodeset + c.before :suite do + # Install module and dependencies + puppet_module_install(:source => proj_root, :module_name => 'mysql') + hosts.each do |host| + # Required for binding tests. + if fact('osfamily') == 'RedHat' + version = fact("operatingsystemmajrelease") + shell("yum localinstall -y http://yum.puppetlabs.com/puppetlabs-release-el-#{version}.noarch.rpm") + end + + shell("/bin/touch #{default['puppetpath']}/hiera.yaml") + shell('puppet module install puppetlabs-stdlib --version 3.2.0', { :acceptable_exit_codes => [0,1] }) + on host, puppet('module','install','stahnma/epel'), { :acceptable_exit_codes => [0,1] } + end + end +end diff --git a/mysql/spec/unit/puppet/functions/mysql_deepmerge_spec.rb b/mysql/spec/unit/puppet/functions/mysql_deepmerge_spec.rb new file mode 100644 index 000000000..fa9c72b78 --- /dev/null +++ b/mysql/spec/unit/puppet/functions/mysql_deepmerge_spec.rb @@ -0,0 +1,91 @@ +#! /usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:mysql_deepmerge) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + describe 'when calling mysql_deepmerge from puppet' do + it "should not compile when no arguments are passed" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = '$x = mysql_deepmerge()' + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /wrong number of arguments/) + end + + it "should not compile when 1 argument is passed" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = "$my_hash={'one' => 1}\n$x = mysql_deepmerge($my_hash)" + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /wrong number of arguments/) + end + end + + describe 'when calling mysql_deepmerge on the scope instance' do + it 'should require all parameters are hashes' do + expect { new_hash = scope.function_mysql_deepmerge([{}, '2'])}.to raise_error(Puppet::ParseError, /unexpected argument type String/) + expect { new_hash = scope.function_mysql_deepmerge([{}, 2])}.to raise_error(Puppet::ParseError, /unexpected argument type Fixnum/) + end + + it 'should accept empty strings as puppet undef' do + expect { new_hash = scope.function_mysql_deepmerge([{}, ''])}.not_to raise_error + end + + it 'should be able to mysql_deepmerge two hashes' do + new_hash = scope.function_mysql_deepmerge([{'one' => '1', 'two' => '1'}, {'two' => '2', 'three' => '2'}]) + new_hash['one'].should == '1' + new_hash['two'].should == '2' + new_hash['three'].should == '2' + end + + it 'should mysql_deepmerge multiple hashes' do + hash = scope.function_mysql_deepmerge([{'one' => 1}, {'one' => '2'}, {'one' => '3'}]) + hash['one'].should == '3' + end + + it 'should accept empty hashes' do + scope.function_mysql_deepmerge([{},{},{}]).should == {} + end + + it 'should mysql_deepmerge subhashes' do + hash = scope.function_mysql_deepmerge([{'one' => 1}, {'two' => 2, 'three' => { 'four' => 4 } }]) + hash['one'].should == 1 + hash['two'].should == 2 + hash['three'].should == { 'four' => 4 } + end + + it 'should append to subhashes' do + hash = scope.function_mysql_deepmerge([{'one' => { 'two' => 2 } }, { 'one' => { 'three' => 3 } }]) + hash['one'].should == { 'two' => 2, 'three' => 3 } + end + + it 'should append to subhashes 2' do + hash = scope.function_mysql_deepmerge([{'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } }, {'two' => 'dos', 'three' => { 'five' => 5 } }]) + hash['one'].should == 1 + hash['two'].should == 'dos' + hash['three'].should == { 'four' => 4, 'five' => 5 } + end + + it 'should append to subhashes 3' do + hash = scope.function_mysql_deepmerge([{ 'key1' => { 'a' => 1, 'b' => 2 }, 'key2' => { 'c' => 3 } }, { 'key1' => { 'b' => 99 } }]) + hash['key1'].should == { 'a' => 1, 'b' => 99 } + hash['key2'].should == { 'c' => 3 } + end + + it 'should equate keys mod dash and underscore' do + hash = scope.function_mysql_deepmerge([{ 'a-b-c' => 1 } , { 'a_b_c' => 10 }]) + hash['a_b_c'].should == 10 + hash.should_not have_key('a-b-c') + end + + it 'should keep style of the last when keys are euqal mod dash and underscore' do + hash = scope.function_mysql_deepmerge([{ 'a-b-c' => 1, 'b_c_d' => { 'c-d-e' => 2, 'e-f-g' => 3 }} , { 'a_b_c' => 10, 'b-c-d' => { 'c_d_e' => 12 } }]) + hash['a_b_c'].should == 10 + hash.should_not have_key('a-b-c') + hash['b-c-d'].should == { 'e-f-g' => 3, 'c_d_e' => 12 } + hash.should_not have_key('b_c_d') + end + end +end diff --git a/mysql/spec/unit/puppet/functions/mysql_password_spec.rb b/mysql/spec/unit/puppet/functions/mysql_password_spec.rb new file mode 100644 index 000000000..073691004 --- /dev/null +++ b/mysql/spec/unit/puppet/functions/mysql_password_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe 'the mysql_password function' do + before :all do + Puppet::Parser::Functions.autoloader.loadall + end + + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + 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(%w(foo bar)) }.should( raise_error(Puppet::ParseError)) + end + + it 'should convert password into a hash' do + result = scope.function_mysql_password(%w(password)) + result.should(eq('*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19')) + end + +end diff --git a/mysql/spec/unit/puppet/provider/database/mysql_spec.rb b/mysql/spec/unit/puppet/provider/database/mysql_spec.rb new file mode 100644 index 000000000..e2557fc35 --- /dev/null +++ b/mysql/spec/unit/puppet/provider/database/mysql_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +provider_class = Puppet::Type.type(:database).provider(:mysql) + +describe provider_class do + subject { provider_class } + + let(:root_home) { '/root' } + let(:defaults_file) { '--defaults-extra-file=/root/.my.cnf' } + + let(:raw_databases) do + <<-SQL_OUTPUT +information_schema +mydb +mysql +performance_schema +test + SQL_OUTPUT + end + + let(:parsed_databases) { %w(information_schema mydb mysql performance_schema test) } + + before :each do + @resource = Puppet::Type::Database.new( + { :charset => 'utf8', :name => 'new_database' } + ) + @provider = provider_class.new(@resource) + Facter.stubs(:value).with(:root_home).returns(root_home) + Puppet::Util.stubs(:which).with('mysql').returns('/usr/bin/mysql') + subject.stubs(:which).with('mysql').returns('/usr/bin/mysql') + subject.stubs(:defaults_file).returns('--defaults-extra-file=/root/.my.cnf') + end + + describe 'self.instances' do + it 'returns an array of databases' do + subject.stubs(:mysql).with([defaults_file, '-NBe', 'show databases']).returns(raw_databases) + + databases = subject.instances.collect {|x| x.name } + parsed_databases.should match_array(databases) + end + end + + describe 'create' do + it 'makes a user' do + subject.expects(:mysql).with([defaults_file, '-NBe', "create database `#{@resource[:name]}` character set #{@resource[:charset]}"]) + @provider.create + end + end + + describe 'destroy' do + it 'removes a user if present' do + subject.expects(:mysqladmin).with([defaults_file, '-f', 'drop', "#{@resource[:name]}"]) + @provider.destroy + end + end + + describe 'charset' do + it 'returns a charset' do + subject.expects(:mysql).with([defaults_file, '-NBe', "show create database `#{@resource[:name]}`"]).returns('mydbCREATE DATABASE `mydb` /*!40100 DEFAULT CHARACTER SET utf8 */') + @provider.charset.should == 'utf8' + end + end + + describe 'charset=' do + it 'changes the charset' do + subject.expects(:mysql).with([defaults_file, '-NBe', "alter database `#{@resource[:name]}` CHARACTER SET blah"]).returns('0') + + @provider.charset=('blah') + end + end + + describe 'exists?' do + it 'checks if user exists' do + subject.expects(:mysql).with([defaults_file, '-NBe', 'show databases']).returns('information_schema\nmydb\nmysql\nperformance_schema\ntest') + @provider.exists? + end + end + + describe 'self.defaults_file' do + it 'sets --defaults-extra-file' do + File.stubs(:file?).with('#{root_home}/.my.cnf').returns(true) + @provider.defaults_file.should == '--defaults-extra-file=/root/.my.cnf' + end + end + +end diff --git a/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb b/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb new file mode 100644 index 000000000..4d9484d04 --- /dev/null +++ b/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb @@ -0,0 +1,95 @@ +require 'puppet' +require 'mocha/api' +require 'spec_helper' +RSpec.configure do |config| + config.mock_with :mocha +end +provider_class = Puppet::Type.type(:database_grant).provider(:mysql) +describe provider_class do + let(:root_home) { '/root' } + + before :each do + @resource = Puppet::Type::Database_grant.new( + { :privileges => 'all', :provider => 'mysql', :name => 'user@host'} + ) + @provider = provider_class.new(@resource) + Facter.stubs(:value).with(:root_home).returns(root_home) + File.stubs(:file?).with("#{root_home}/.my.cnf").returns(true) + end + + it 'should query privileges from the database' do + provider_class.expects(:mysql) .with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-Be', 'describe user']).returns <<-EOT +Field Type Null Key Default Extra +Host char(60) NO PRI +User char(16) NO PRI +Password char(41) NO +Select_priv enum('N','Y') NO N +Insert_priv enum('N','Y') NO N +Update_priv enum('N','Y') NO N +EOT + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-Be', 'describe db']).returns <<-EOT +Field Type Null Key Default Extra +Host char(60) NO PRI +Db char(64) NO PRI +User char(16) NO PRI +Select_priv enum('N','Y') NO N +Insert_priv enum('N','Y') NO N +Update_priv enum('N','Y') NO N +EOT + provider_class.user_privs.should == %w(Select_priv Insert_priv Update_priv) + provider_class.db_privs.should == %w(Select_priv Insert_priv Update_priv) + end + + it 'should query set privileges' do + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-Be', "select * from mysql.user where user='user' and host='host'"]).returns <<-EOT +Host User Password Select_priv Insert_priv Update_priv +host user Y N Y +EOT + @provider.privileges.should == %w(Select_priv Update_priv) + end + + it 'should recognize when all privileges are set' do + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-Be', "select * from mysql.user where user='user' and host='host'"]).returns <<-EOT +Host User Password Select_priv Insert_priv Update_priv +host user Y Y Y +EOT + @provider.all_privs_set?.should == true + end + + it 'should recognize when all privileges are not set' do + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-Be', "select * from mysql.user where user='user' and host='host'"]).returns <<-EOT +Host User Password Select_priv Insert_priv Update_priv +host user Y N Y +EOT + @provider.all_privs_set?.should == false + end + + it 'should be able to set all privileges' do + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-NBe', "SELECT '1' FROM user WHERE user='user' AND host='host'"]).returns "1\n" + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-Be', "update user set Select_priv = 'Y', Insert_priv = 'Y', Update_priv = 'Y' where user='user' and host='host'"]) + provider_class.expects(:mysqladmin).with(%W(--defaults-extra-file=#{root_home}/.my.cnf flush-privileges)) + @provider.privileges=(%w(all)) + end + + it 'should be able to set partial privileges' do + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-NBe', "SELECT '1' FROM user WHERE user='user' AND host='host'"]).returns "1\n" + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-Be', "update user set Select_priv = 'Y', Insert_priv = 'N', Update_priv = 'Y' where user='user' and host='host'"]) + provider_class.expects(:mysqladmin).with(%W(--defaults-extra-file=#{root_home}/.my.cnf flush-privileges)) + @provider.privileges=(%w(Select_priv Update_priv)) + end + + it 'should be case insensitive' do + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-NBe', "SELECT '1' FROM user WHERE user='user' AND host='host'"]).returns "1\n" + provider_class.expects(:mysql).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'mysql', '-Be', "update user set Select_priv = 'Y', Insert_priv = 'Y', Update_priv = 'Y' where user='user' and host='host'"]) + provider_class.expects(:mysqladmin).with(["--defaults-extra-file=#{root_home}/.my.cnf", 'flush-privileges']) + @provider.privileges=(%w(SELECT_PRIV insert_priv UpDaTe_pRiV)) + end + + it 'should not pass --defaults-extra-file if $root_home/.my.cnf is absent' do + File.stubs(:file?).with("#{root_home}/.my.cnf").returns(false) + 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(%w(flush-privileges)) + @provider.privileges=(%w(Select_priv Update_priv)) + end +end diff --git a/mysql/spec/unit/puppet/provider/database_user/mysql_spec.rb b/mysql/spec/unit/puppet/provider/database_user/mysql_spec.rb new file mode 100644 index 000000000..d85306822 --- /dev/null +++ b/mysql/spec/unit/puppet/provider/database_user/mysql_spec.rb @@ -0,0 +1,119 @@ +require 'spec_helper' + +provider_class = Puppet::Type.type(:database_user).provider(:mysql) + +describe provider_class do + subject { provider_class } + + let(:root_home) { '/root' } + let(:defaults_file) { '--defaults-extra-file=/root/.my.cnf' } + let(:newhash) { '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' } + + let(:raw_users) do + <<-SQL_OUTPUT +root@127.0.0.1 +root@::1 +@localhost +debian-sys-maint@localhost +root@localhost +usvn_user@localhost +@vagrant-ubuntu-raring-64 + SQL_OUTPUT + end + + let(:parsed_users) { %w(root@127.0.0.1 root@::1 debian-sys-maint@localhost root@localhost usvn_user@localhost) } + + before :each do + # password hash = mypass + @resource = Puppet::Type::Database_user.new( + { :password_hash => '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4', + :name => 'joe@localhost', + :max_user_connections => '10' + } + ) + @provider = provider_class.new(@resource) + Facter.stubs(:value).with(:root_home).returns(root_home) + Puppet::Util.stubs(:which).with('mysql').returns('/usr/bin/mysql') + subject.stubs(:which).with('mysql').returns('/usr/bin/mysql') + subject.stubs(:defaults_file).returns('--defaults-extra-file=/root/.my.cnf') + end + + describe 'self.instances' do + it 'returns an array of users' do + subject.stubs(:mysql).with([defaults_file, 'mysql', "-BNeselect concat(User, '@',Host) as User from mysql.user"]).returns(raw_users) + + usernames = subject.instances.collect {|x| x.name } + parsed_users.should match_array(usernames) + end + end + + describe 'create' do + it 'makes a user' do + subject.expects(:mysql).with([defaults_file, 'mysql', '-e', "grant usage on *.* to 'joe'@'localhost' identified by PASSWORD + '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4' with max_user_connections 10"]) + @provider.expects(:exists?).returns(true) + @provider.create.should be_truthy + end + end + + describe 'destroy' do + it 'removes a user if present' do + subject.expects(:mysql).with([defaults_file, 'mysql', '-e', "drop user 'joe'@'localhost'"]) + @provider.expects(:exists?).returns(false) + @provider.destroy.should be_truthy + end + end + + describe 'password_hash' do + it 'returns a hash' do + subject.expects(:mysql).with([defaults_file, 'mysql', '-NBe', "select password from mysql.user where CONCAT(user, '@', host) = 'joe@localhost'"]).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4') + @provider.password_hash.should == '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4' + end + end + + describe 'password_hash=' do + it 'changes the hash' do + subject.expects(:mysql).with([defaults_file, 'mysql', '-e', "SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'"]).returns('0') + + @provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') + @provider.password_hash=('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') + end + end + + describe 'max_user_connections' do + it 'returns max user connections' do + subject.expects(:mysql).with([defaults_file, 'mysql', '-NBe', "select max_user_connections from mysql.user where CONCAT(user, '@', host) = 'joe@localhost'"]).returns('10') + @provider.max_user_connections.should == '10' + end + end + + describe 'max_user_connections=' do + it 'changes max user connections' do + subject.expects(:mysql).with([defaults_file, 'mysql', '-e', "grant usage on *.* to 'joe'@'localhost' with max_user_connections 42"]).returns('0') + @provider.expects(:max_user_connections).returns('42') + @provider.max_user_connections=('42') + end + end + + describe 'exists?' do + it 'checks if user exists' do + subject.expects(:mysql).with([defaults_file, 'mysql', '-NBe', "select '1' from mysql.user where CONCAT(user, '@', host) = 'joe@localhost'"]).returns('1') + @provider.exists?.should be_truthy + end + end + + describe 'flush' do + it 'removes cached privileges' do + subject.expects(:mysqladmin).with([defaults_file, 'flush-privileges']) + @provider.flush + end + end + + describe 'self.defaults_file' do + it 'sets --defaults-extra-file' do + File.stubs(:file?).with('#{root_home}/.my.cnf').returns(true) + @provider.defaults_file.should == '--defaults-extra-file=/root/.my.cnf' + end + end + +end diff --git a/mysql/spec/unit/puppet/provider/mysql_database/mysql_spec.rb b/mysql/spec/unit/puppet/provider/mysql_database/mysql_spec.rb new file mode 100644 index 000000000..4bc24b46c --- /dev/null +++ b/mysql/spec/unit/puppet/provider/mysql_database/mysql_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +describe Puppet::Type.type(:mysql_database).provider(:mysql) do + + let(:defaults_file) { '--defaults-extra-file=/root/.my.cnf' } + + let(:raw_databases) do + <<-SQL_OUTPUT +information_schema +mydb +mysql +performance_schema +test + SQL_OUTPUT + end + + let(:parsed_databases) { %w(information_schema mydb mysql performance_schema test) } + + let(:resource) { Puppet::Type.type(:mysql_database).new( + { :ensure => :present, + :charset => 'latin1', + :collate => 'latin1_swedish_ci', + :name => 'new_database', + :provider => described_class.name + } + )} + let(:provider) { resource.provider } + + before :each do + Facter.stubs(:value).with(:root_home).returns('/root') + Puppet::Util.stubs(:which).with('mysql').returns('/usr/bin/mysql') + File.stubs(:file?).with('/root/.my.cnf').returns(true) + provider.class.stubs(:mysql).with([defaults_file, '-NBe', 'show databases']).returns('new_database') + provider.class.stubs(:mysql).with([defaults_file, '-NBe', "show variables like '%_database'", 'new_database']).returns("character_set_database latin1\ncollation_database latin1_swedish_ci\nskip_show_database OFF") + end + + let(:instance) { provider.class.instances.first } + + describe 'self.instances' do + it 'returns an array of databases' do + provider.class.stubs(:mysql).with([defaults_file, '-NBe', 'show databases']).returns(raw_databases) + raw_databases.each_line do |db| + provider.class.stubs(:mysql).with([defaults_file, '-NBe', "show variables like '%_database'", db.chomp]).returns("character_set_database latin1\ncollation_database latin1_swedish_ci\nskip_show_database OFF") + end + databases = provider.class.instances.collect {|x| x.name } + parsed_databases.should match_array(databases) + end + end + + describe 'self.prefetch' do + it 'exists' do + provider.class.instances + provider.class.prefetch({}) + end + end + + describe 'create' do + it 'makes a database' do + provider.expects(:mysql).with([defaults_file, '-NBe', "create database if not exists `#{resource[:name]}` character set #{resource[:charset]} collate #{resource[:collate]}"]) + provider.expects(:exists?).returns(true) + provider.create.should be_truthy + end + end + + describe 'destroy' do + it 'removes a database if present' do + provider.expects(:mysql).with([defaults_file, '-NBe', "drop database if exists `#{resource[:name]}`"]) + provider.expects(:exists?).returns(false) + provider.destroy.should be_truthy + end + end + + describe 'exists?' do + it 'checks if database exists' do + instance.exists?.should be_truthy + end + end + + describe 'self.defaults_file' do + it 'sets --defaults-extra-file' do + File.stubs(:file?).with('/root/.my.cnf').returns(true) + provider.defaults_file.should eq '--defaults-extra-file=/root/.my.cnf' + end + it 'fails if file missing' do + File.stubs(:file?).with('/root/.my.cnf').returns(false) + provider.defaults_file.should be_nil + end + end + + describe 'charset' do + it 'returns a charset' do + instance.charset.should == 'latin1' + end + end + + describe 'charset=' do + it 'changes the charset' do + provider.expects(:mysql).with([defaults_file, '-NBe', "alter database `#{resource[:name]}` CHARACTER SET blah"]).returns('0') + + provider.charset=('blah') + end + end + + describe 'collate' do + it 'returns a collate' do + instance.collate.should == 'latin1_swedish_ci' + end + end + + describe 'collate=' do + it 'changes the collate' do + provider.expects(:mysql).with([defaults_file, '-NBe', "alter database `#{resource[:name]}` COLLATE blah"]).returns('0') + + provider.collate=('blah') + end + end + +end diff --git a/mysql/spec/unit/puppet/provider/mysql_user/mysql_spec.rb b/mysql/spec/unit/puppet/provider/mysql_user/mysql_spec.rb new file mode 100644 index 000000000..50f0c3bd2 --- /dev/null +++ b/mysql/spec/unit/puppet/provider/mysql_user/mysql_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe Puppet::Type.type(:mysql_user).provider(:mysql) do + let(:defaults_file) { '--defaults-extra-file=/root/.my.cnf' } + let(:newhash) { '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' } + + let(:raw_users) do + <<-SQL_OUTPUT +root@127.0.0.1 +root@::1 +@localhost +debian-sys-maint@localhost +root@localhost +usvn_user@localhost +@vagrant-ubuntu-raring-64 + SQL_OUTPUT + end + + let(:parsed_users) { %w(root@127.0.0.1 root@::1 @localhost debian-sys-maint@localhost root@localhost usvn_user@localhost @vagrant-ubuntu-raring-64) } + + let(:resource) { Puppet::Type.type(:mysql_user).new( + { :ensure => :present, + :password_hash => '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4', + :name => 'joe@localhost', + :max_user_connections => '10', + :max_connections_per_hour => '10', + :max_queries_per_hour => '10', + :max_updates_per_hour => '10', + :provider => described_class.name + } + )} + let(:provider) { resource.provider } + + before :each do + # Set up the stubs for an instances call. + Facter.stubs(:value).with(:root_home).returns('/root') + Puppet::Util.stubs(:which).with('mysql').returns('/usr/bin/mysql') + File.stubs(:file?).with('/root/.my.cnf').returns(true) + provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns('joe@localhost') + provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD FROM mysql.user WHERE CONCAT(user, '@', host) = 'joe@localhost'"]).returns('10 10 10 10 *6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4') + end + + let(:instance) { provider.class.instances.first } + + describe 'self.instances' do + it 'returns an array of users' do + provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns(raw_users) + parsed_users.each do |user| + provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'"]).returns('10 10 10 10 ') + end + + usernames = provider.class.instances.collect {|x| x.name } + parsed_users.should match_array(usernames) + end + end + + describe 'self.prefetch' do + it 'exists' do + provider.class.instances + provider.class.prefetch({}) + end + end + + describe 'create' do + it 'makes a user' do + provider.expects(:mysql).with([defaults_file, '-e', "GRANT USAGE ON *.* TO 'joe'@'localhost' IDENTIFIED BY PASSWORD '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4' WITH MAX_USER_CONNECTIONS 10 MAX_CONNECTIONS_PER_HOUR 10 MAX_QUERIES_PER_HOUR 10 MAX_UPDATES_PER_HOUR 10"]) + provider.expects(:exists?).returns(true) + provider.create.should be_truthy + end + end + + describe 'destroy' do + it 'removes a user if present' do + provider.expects(:mysql).with([defaults_file, '-e', "DROP USER 'joe'@'localhost'"]) + provider.expects(:exists?).returns(false) + provider.destroy.should be_truthy + end + end + + describe 'exists?' do + it 'checks if user exists' do + instance.exists?.should be_truthy + end + end + + describe 'self.defaults_file' do + it 'sets --defaults-extra-file' do + File.stubs(:file?).with('/root/.my.cnf').returns(true) + provider.defaults_file.should eq '--defaults-extra-file=/root/.my.cnf' + end + it 'fails if file missing' do + File.expects(:file?).with('/root/.my.cnf').returns(false) + provider.defaults_file.should be_nil + end + end + + describe 'password_hash' do + it 'returns a hash' do + instance.password_hash.should == '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4' + end + end + + describe 'password_hash=' do + it 'changes the hash' do + provider.expects(:mysql).with([defaults_file, '-e', "SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'"]).returns('0') + + provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') + provider.password_hash=('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') + end + end + + ['max_user_connections', 'max_connections_per_hour', 'max_queries_per_hour', + 'max_updates_per_hour'].each do |property| + + describe property do + it "returns #{property}" do + instance.send("#{property}".to_sym).should == '10' + end + end + + describe "#{property}=" do + it "changes #{property}" do + provider.expects(:mysql).with([defaults_file, '-e', "GRANT USAGE ON *.* TO 'joe'@'localhost' WITH #{property.upcase} 42"]).returns('0') + provider.expects(property.to_sym).returns('42') + provider.send("#{property}=".to_sym, '42') + end + end + end + +end diff --git a/mysql/spec/unit/puppet/type/mysql_database_spec.rb b/mysql/spec/unit/puppet/type/mysql_database_spec.rb new file mode 100644 index 000000000..e2ebd90d4 --- /dev/null +++ b/mysql/spec/unit/puppet/type/mysql_database_spec.rb @@ -0,0 +1,29 @@ +require 'puppet' +require 'puppet/type/mysql_database' +describe Puppet::Type.type(:mysql_database) do + + before :each do + @user = Puppet::Type.type(:mysql_database).new(:name => 'test', :charset => 'utf8', :collate => 'utf8_blah_ci') + end + + it 'should accept a database name' do + @user[:name].should == 'test' + end + + it 'should accept a charset' do + @user[:charset] = 'latin1' + @user[:charset].should == 'latin1' + end + + it 'should accept a collate' do + @user[:collate] = 'latin1_swedish_ci' + @user[:collate].should == 'latin1_swedish_ci' + end + + it 'should require a name' do + expect { + Puppet::Type.type(:mysql_database).new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + +end diff --git a/mysql/spec/unit/puppet/type/mysql_grant_spec.rb b/mysql/spec/unit/puppet/type/mysql_grant_spec.rb new file mode 100644 index 000000000..4171ab28e --- /dev/null +++ b/mysql/spec/unit/puppet/type/mysql_grant_spec.rb @@ -0,0 +1,50 @@ +require 'puppet' +require 'puppet/type/mysql_grant' +describe Puppet::Type.type(:mysql_grant) do + + before :each do + @user = Puppet::Type.type(:mysql_grant).new(:name => 'foo@localhost/*.*', :privileges => ['ALL', 'PROXY'], :table => ['*.*','@'], :user => 'foo@localhost') + end + + it 'should accept a grant name' do + @user[:name].should == 'foo@localhost/*.*' + end + + it 'should accept ALL privileges' do + @user[:privileges] = 'ALL' + @user[:privileges].should == ['ALL'] + end + + it 'should accept PROXY privilege' do + @user[:privileges] = 'PROXY' + @user[:privileges].should == ['PROXY'] + end + + it 'should accept a table' do + @user[:table] = '*.*' + @user[:table].should == '*.*' + end + + it 'should accept @ for table' do + @user[:table] = '@' + @user[:table].should == '@' + end + + it 'should accept a user' do + @user[:user] = 'foo@localhost' + @user[:user].should == 'foo@localhost' + end + + it 'should require a name' do + expect { + Puppet::Type.type(:mysql_grant).new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + + it 'should require the name to match the user and table' do + expect { + Puppet::Type.type(:mysql_grant).new(:name => 'foo', :privileges => ['ALL', 'PROXY'], :table => ['*.*','@'], :user => 'foo@localhost') + }.to raise_error /name must match user and table parameters/ + end + +end diff --git a/mysql/spec/unit/puppet/type/mysql_user_spec.rb b/mysql/spec/unit/puppet/type/mysql_user_spec.rb new file mode 100644 index 000000000..7ffc801b6 --- /dev/null +++ b/mysql/spec/unit/puppet/type/mysql_user_spec.rb @@ -0,0 +1,42 @@ +require 'puppet' +require 'puppet/type/mysql_user' +describe Puppet::Type.type(:mysql_user) do + + it 'should fail with a long user name' do + expect { + Puppet::Type.type(:mysql_user).new({:name => '12345678901234567@localhost', :password_hash => 'pass'}) + }.to raise_error /MySQL usernames are limited to a maximum of 16 characters/ + end + + it 'should require a name' do + expect { + Puppet::Type.type(:mysql_user).new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + + context 'using foo@localhost' do + before :each do + @user = Puppet::Type.type(:mysql_user).new(:name => 'foo@localhost', :password_hash => 'pass') + end + + it 'should accept a user name' do + @user[:name].should == 'foo@localhost' + end + + it 'should accept a password' do + @user[:password_hash] = 'foo' + @user[:password_hash].should == 'foo' + end + end + + context 'using foo@LocalHost' do + before :each do + @user = Puppet::Type.type(:mysql_user).new(:name => 'foo@LocalHost', :password_hash => 'pass') + end + + it 'should lowercase the user name' do + @user[:name].should == 'foo@localhost' + end + + end +end diff --git a/mysql/templates/my.cnf.erb b/mysql/templates/my.cnf.erb new file mode 100644 index 000000000..8c2001ce8 --- /dev/null +++ b/mysql/templates/my.cnf.erb @@ -0,0 +1,22 @@ +<% @options.sort.map do |k,v| -%> +<% if v.is_a?(Hash) -%> +[<%= k %>] +<% v.sort.map do |ki, vi| -%> +<% if ki == 'ssl-disable' or (ki =~ /^ssl/ and v['ssl-disable'] == true) -%> +<% next %> +<% elsif vi == true or v == '' -%> +<%= ki %> +<% elsif vi.is_a?(Array) -%> +<% vi.each do |vii| -%> +<%= ki %> = <%= vii %> +<% end -%> +<% elsif vi != :undef -%> +<%= ki %> = <%= vi %> +<% end -%> +<% end -%> +<% end %> +<% end -%> + +<% if @includedir and @includedir != '' %> +!includedir <%= @includedir %> +<% end %> diff --git a/mysql/templates/my.cnf.pass.erb b/mysql/templates/my.cnf.pass.erb new file mode 100644 index 000000000..99663fccd --- /dev/null +++ b/mysql/templates/my.cnf.pass.erb @@ -0,0 +1,7 @@ +[client] +user=root +host=localhost +<% unless scope.lookupvar('mysql::server::root_password') == 'UNSET' -%> +password='<%= scope.lookupvar('mysql::server::root_password') %>' +<% end -%> +socket=<%= @options['client']['socket'] -%> diff --git a/mysql/templates/my.conf.cnf.erb b/mysql/templates/my.conf.cnf.erb new file mode 100644 index 000000000..04d297429 --- /dev/null +++ b/mysql/templates/my.conf.cnf.erb @@ -0,0 +1,17 @@ +### MANAGED BY PUPPET ### +<% @settings.sort.each do |section, content| -%> +[<%= section %>] +<% content.sort.each do |key, values| -%> +<% [values].flatten.sort.each do |value| -%> +<%= !value ? '#' : '' %><%= key -%><%= + case value + when true, false + '' + else + " = #{value}" + end +%> +<% end -%> +<% end -%> + +<% end -%> diff --git a/mysql/templates/mysqlbackup.sh.erb b/mysql/templates/mysqlbackup.sh.erb new file mode 100755 index 000000000..68c911fd5 --- /dev/null +++ b/mysql/templates/mysqlbackup.sh.erb @@ -0,0 +1,72 @@ +#!/bin/bash +# +# 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 %> +ROTATE=<%= [ Integer(@backuprotate) - 1, 0 ].max %> + +PREFIX=mysql_backup_ +<% if @ignore_events %> +EVENTS="--ignore-table=mysql.event" +<% else %> +EVENTS="--events" +<% end %> + +##### STOP CONFIG #################################################### +PATH=<%= @execpath %> + + + +set -o pipefail + +cleanup() +{ + find "${DIR}/" -maxdepth 1 -type f -name "${PREFIX}*.sql*" -mtime +${ROTATE} -print0 | xargs -0 -r rm -f +} + +<% if @delete_before_dump -%> +cleanup + +<% end -%> +<% if @backupdatabases.empty? -%> +<% if @file_per_database -%> +mysql -s -r -N -e 'SHOW DATABASES' | while read dbname +do + mysqldump -u${USER} -p${PASS} --opt --flush-logs --single-transaction \ + ${EVENTS} \ + ${dbname} <% if @backupcompress %>| bzcat -zc <% end %>> ${DIR}/${PREFIX}${dbname}_`date +%Y%m%d-%H%M%S`.sql<% if @backupcompress %>.bz2<% end %> +done +<% else -%> +mysqldump -u${USER} -p${PASS} --opt --flush-logs --single-transaction \ + ${EVENTS} \ + --all-databases <% if @backupcompress %>| bzcat -zc <% end %>> ${DIR}/${PREFIX}`date +%Y%m%d-%H%M%S`.sql<% if @backupcompress %>.bz2<% end %> +<% end -%> +<% else -%> +<% @backupdatabases.each do |db| -%> +mysqldump -u${USER} -p${PASS} --opt --flush-logs --single-transaction \ + ${EVENTS} \ + <%= db %><% if @backupcompress %>| bzcat -zc <% end %>> ${DIR}/${PREFIX}<%= db %>_`date +%Y%m%d-%H%M%S`.sql<% if @backupcompress %>.bz2<% end %> +<% end -%> +<% end -%> + +<% unless @delete_before_dump -%> +if [ $? -eq 0 ] ; then + cleanup +fi +<% end -%> + +<% if @postscript -%> + <%- [@postscript].flatten.compact.each do |script|%> +<%= script %> + <%- end -%> +<% end -%> diff --git a/mysql/tests/backup.pp b/mysql/tests/backup.pp new file mode 100644 index 000000000..cb669e6db --- /dev/null +++ b/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/mysql/tests/bindings.pp b/mysql/tests/bindings.pp new file mode 100644 index 000000000..83af3713a --- /dev/null +++ b/mysql/tests/bindings.pp @@ -0,0 +1,3 @@ +class { 'mysql::bindings': + php_enable => 'true', +} diff --git a/mysql/tests/init.pp b/mysql/tests/init.pp new file mode 100644 index 000000000..846121b7d --- /dev/null +++ b/mysql/tests/init.pp @@ -0,0 +1 @@ +include mysql diff --git a/mysql/tests/java.pp b/mysql/tests/java.pp new file mode 100644 index 000000000..0fc009a6d --- /dev/null +++ b/mysql/tests/java.pp @@ -0,0 +1 @@ +class { 'mysql::java':} diff --git a/mysql/tests/mysql_database.pp b/mysql/tests/mysql_database.pp new file mode 100644 index 000000000..8747f707d --- /dev/null +++ b/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/mysql/tests/mysql_db.pp b/mysql/tests/mysql_db.pp new file mode 100644 index 000000000..1bb3592e4 --- /dev/null +++ b/mysql/tests/mysql_db.pp @@ -0,0 +1,17 @@ +class { 'mysql::server': + config_hash => {'root_password' => 'password'} +} +mysql::db { 'mydb': + user => 'myuser', + password => 'mypass', + host => 'localhost', + grant => ['SELECT', 'UPDATE'], +} +mysql::db { "mydb_${fqdn}": + user => 'myuser', + password => 'mypass', + dbname => 'mydb', + host => $::fqdn, + grant => ['SELECT', 'UPDATE'], + tag => $domain, +} diff --git a/mysql/tests/mysql_grant.pp b/mysql/tests/mysql_grant.pp new file mode 100644 index 000000000..20fe78d6a --- /dev/null +++ b/mysql/tests/mysql_grant.pp @@ -0,0 +1,5 @@ +mysql_grant{'test1@localhost/redmine.*': + user => 'test1@localhost', + table => 'redmine.*', + privileges => ['UPDATE'], +} diff --git a/mysql/tests/mysql_user.pp b/mysql/tests/mysql_user.pp new file mode 100644 index 000000000..70a6e1672 --- /dev/null +++ b/mysql/tests/mysql_user.pp @@ -0,0 +1,23 @@ +$mysql_root_pw = 'password' + +class { 'mysql::server': + config_hash => { + root_password => 'password', + } +} + +mysql_user{ 'redmine@localhost': + ensure => present, + password_hash => mysql_password('redmine'), + require => Class['mysql::server'], +} + +mysql_user{ 'dan@localhost': + ensure => present, + password_hash => mysql_password('blah') +} + +mysql_user{ 'dan@%': + ensure => present, + password_hash => mysql_password('blah'), +} diff --git a/mysql/tests/perl.pp b/mysql/tests/perl.pp new file mode 100644 index 000000000..87e941751 --- /dev/null +++ b/mysql/tests/perl.pp @@ -0,0 +1 @@ +include mysql::perl diff --git a/mysql/tests/python.pp b/mysql/tests/python.pp new file mode 100644 index 000000000..04f7ffa1a --- /dev/null +++ b/mysql/tests/python.pp @@ -0,0 +1 @@ +class { 'mysql::python':} diff --git a/mysql/tests/ruby.pp b/mysql/tests/ruby.pp new file mode 100644 index 000000000..e84c046a3 --- /dev/null +++ b/mysql/tests/ruby.pp @@ -0,0 +1 @@ +include mysql::ruby diff --git a/mysql/tests/server.pp b/mysql/tests/server.pp new file mode 100644 index 000000000..8afdd00d2 --- /dev/null +++ b/mysql/tests/server.pp @@ -0,0 +1,3 @@ +class { 'mysql::server': + root_password => 'password', +} diff --git a/mysql/tests/server/account_security.pp b/mysql/tests/server/account_security.pp new file mode 100644 index 000000000..de393cce4 --- /dev/null +++ b/mysql/tests/server/account_security.pp @@ -0,0 +1,4 @@ +class { 'mysql::server': + config_hash => { 'root_password' => 'password', }, +} +class { 'mysql::server::account_security': } diff --git a/mysql/tests/server/config.pp b/mysql/tests/server/config.pp new file mode 100644 index 000000000..fe8d86e90 --- /dev/null +++ b/mysql/tests/server/config.pp @@ -0,0 +1,11 @@ +mysql::server::config { 'testfile': + settings => { + 'mysqld' => { + 'bind-address' => '0.0.0.0', + 'read-only' => true, + }, + 'client' => { + 'port' => '3306' + } + } +} diff --git a/n1k-vsm b/n1k-vsm deleted file mode 160000 index 69ff09406..000000000 --- a/n1k-vsm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 69ff094069506f98431182c6097b3b6b9ea6fdb9 diff --git a/n1k-vsm/README.md b/n1k-vsm/README.md new file mode 100644 index 000000000..54f9b892f --- /dev/null +++ b/n1k-vsm/README.md @@ -0,0 +1,4 @@ +puppet-n1k-vsm +============== + +VSM deployment code for N1k. diff --git a/n1k-vsm/files/repackiso.py b/n1k-vsm/files/repackiso.py new file mode 100755 index 000000000..f768fa835 --- /dev/null +++ b/n1k-vsm/files/repackiso.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +import shutil, tempfile, os, optparse, logging +import sys + +usage = "usage: %prog [options]" +parser = optparse.OptionParser(usage=usage) +parser.add_option("-i", "--isofile", help="ISO image", dest="isoimg") +parser.add_option("-d", "--domainid", help="Domain id ", dest="domainid") +parser.add_option("-n", "--vsmname", help="VSM name", dest="vsmname") +parser.add_option("-m", "--mgmtip", help="Management Ip address", dest="mgmtip") +parser.add_option("-s", "--mgmtsubnet", help="Management Subnet", dest="mgmtsubnet") +parser.add_option("-g", "--gateway", help="Management gateway", dest="mgmtgateway") +parser.add_option("-p", "--password", help="Admin account password", dest="adminpasswd") +parser.add_option("-r", "--vsmrole", help="VSM Role, primary ,secondary or standalone", dest="vsmrole") +parser.add_option("-f", "--file", help="Repackaged file", dest="repackediso") +(options, args) = parser.parse_args() + +isoimg = options.isoimg +domainid = int(options.domainid) +vsmname = options.vsmname +mgmtip = options.mgmtip +mgmtsubnet = options.mgmtsubnet +mgmtgateway = options.mgmtgateway +adminpasswd = options.adminpasswd +vsmrole = options.vsmrole +repackediso = options.repackediso + + +class Command(object): + """Run a command and capture it's output string, error string and exit status""" + def __init__(self, command): + self.command = command + + def run(self, shell=True): + import subprocess as sp + process = sp.Popen(self.command, shell = shell, stdout = sp.PIPE, stderr = sp.PIPE) + self.pid = process.pid + self.output, self.error = process.communicate() + self.failed = process.returncode + return self + + @property + def returncode(self): + return self.failed + +def createOvfEnvXmlFile(domain, gateway, hostname, ip, subnet, password, vsm_mode): + #TODO: write a proper xml + ovf_f = tempfile.NamedTemporaryFile(delete=False) + + st = ' \n' + st += ' $ctrlbridge, +# ensure => present +# } +# +# tapint {"$mgmttap": +# bridge => $mgmtbridge, +# ensure => present +# } +# +# tapint {"$pkttap": +# bridge => $pktbridge, +# ensure => present +# } + + + $diskfile = "/var/spool/vsm/${n1k_vsm::role}_disk" + + exec { "Exec_create_disk": + command => "/usr/bin/qemu-img create -f raw $diskfile ${n1k_vsm::disksize}G", + unless => "/usr/bin/virsh list | grep -c ' ${n1k_vsm::vsmname} .* running'", + } + -> + exec {"Debug_Exec_create_disk_debug": + command => "${n1k_vsm::Debug_Print} \"[INFO]\nExec_create_disk /usr/bin/qemu-img create -f raw $diskfile ${n1k_vsm::disksize}G\" >> ${n1k_vsm::Debug_Log}", + } + + $targetxmlfile = "/var/spool/vsm/vsm_${n1k_vsm::role}_deploy.xml" + file { "File_Target_XML_File": + path => "$targetxmlfile", + owner => 'root', + group => 'root', + mode => '666', + content => template('n1k_vsm/vsm_vm.xml.erb'), + require => Exec["Exec_create_disk"], + } + -> + exec {"Debug_File_Target_XML_FILE": + command => "${n1k_vsm::Debug_Print} \"[INFO]\nFile_Target_XML_File\n path=$targetxmlfile \n owner=root \n group=root \n mode=666 \n\" >> ${n1k_vsm::Debug_Log}", + } + + exec { "Exec_Create_VSM": + command => "/usr/bin/virsh define $targetxmlfile", + unless => "/usr/bin/virsh list | grep -c ' ${n1k_vsm::vsmname} .* running'", + } + -> + exec {"Debug_Exec_Create_VSM": + command => "${n1k_vsm::Debug_Print} \"[INFO]\nExec_Launch_VSM \n command=/bin/echo /usr/bin/virsh define $targetxmlfile \n unless=/usr/bin/virsh list --all | grep -c ' ${n1k_vsm::vsmname} ' \" >> ${n1k_vsm::Debug_Log}", + } + + exec { "Exec_Launch_VSM": + command => "/usr/bin/virsh start ${n1k_vsm::vsmname}", + unless => "/usr/bin/virsh list --all | grep -c ' ${n1k_vsm::vsmname} .* running '", + } + -> + exec {"Debug_Exec_Launch_VSM": + command => "${n1k_vsm::Debug_Print} \"[INFO]\nExec_Launch_VSM \n command=/bin/echo /usr/bin/virsh start ${n1k_vsm::vsmname} \n unless=/usr/bin/virsh list --all | grep -c ' ${n1k_vsm::vsmname} .* running' \" >> ${n1k_vsm::Debug_Log}", + } + + Exec["Exec_create_disk"] -> File["File_Target_XML_File"] -> Exec["Exec_Create_VSM"] -> Exec["Exec_Launch_VSM"] +} + diff --git a/n1k-vsm/manifests/init.pp b/n1k-vsm/manifests/init.pp new file mode 100644 index 000000000..b546f9644 --- /dev/null +++ b/n1k-vsm/manifests/init.pp @@ -0,0 +1,46 @@ +class n1k_vsm( + $configureovs = false, + $ovsbridge, + $physicalinterfaceforovs = 'enp1s0f0', + $nodeip, + $nodenetmask, + $nodegateway, + $vsmname, + $consolepts = 2, + $role = 'primary', + $domainid, + $adminpasswd, + $mgmtip, + $mgmtnetmask, + $mgmtgateway, + $ctrlinterface, + $mgmtinterface, + $pktinterface, + $memory = 4096000, + $vcpu = 2, + $disksize = 4, + $n1kv_source = "puppet:///modules/n1k_vsm/vsm.iso", + $n1kv_version = "latest", + ) +{ + + $imgfile = "/var/spool/vsm/${role}_repacked.iso" + $diskfile = "/var/spool/vsm/${role}_disk" + + $Debug_Print = "/usr/bin/printf" + $Debug_Log = "/tmp/n1kv_vsm_puppet.log" + + # + # Clean up debug log + # + file {"File_$Debug_Log": + path => $Debug_Log, + ensure => "absent", + } + + include n1k_vsm::pkgprep_ovscfg + include n1k_vsm::vsmprep + include n1k_vsm::deploy + + File["File_$Debug_Log"] -> Class['n1k_vsm::pkgprep_ovscfg'] -> Class['n1k_vsm::vsmprep'] -> Class['n1k_vsm::deploy'] +} diff --git a/n1k-vsm/manifests/pkgprep_ovscfg.pp b/n1k-vsm/manifests/pkgprep_ovscfg.pp new file mode 100644 index 000000000..9ee8cb925 --- /dev/null +++ b/n1k-vsm/manifests/pkgprep_ovscfg.pp @@ -0,0 +1,264 @@ +class n1k_vsm::pkgprep_ovscfg { + + # Definition of sync points + + $Sync_Point_KVM = "##SYNC_POINT_KVM" + $Sync_Point_Virsh_Network = "##SYNC_POINT_VIRSH_NETWORK" + + case "$::osfamily" { + "RedHat": { + # + # Order indepedent resources + # + service {"Service_network": + name => "network", + ensure => "running", + restart => "/sbin/service network restart || /bin/true", + } + -> + exec {"Debug_Service_network": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Service_network\n name=network\n ensure=running\n enable=true\n restart=/sbin/service network restart\n\" >> ${n1k_vsm::Debug_Log}", + } + # VSM dependent packages installation section + # + # Eng note + # cwchang: Ideally we should have either of this logic + # 1. Have an iteration thru the package list in the $pkgs.each ... + # Somehow this syntax needs to turn on future parser by document + # 2. package resource should be able to run a name list + # Neither one works. We go for rudimentary one-by-one here for now. + # Pitfalls observed: + # 1. We cannot reassign variables for some reason + # 2. We cannot leave spaces in name + # qemu-kvm-rhev + package {"Package_qemu-kvm": + name => "qemu-kvm", + ensure => "installed", + before => Notify["$Sync_Point_KVM"], + } + -> + exec {"Debug_Package_qemu-kvm": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_qemu-kvm \n name=qemu-kvm \n ensure=installed\n\" >> ${n1k_vsm::Debug_Log}", + } + + package {"Package_virt-viewer": + name => "virt-viewer", + ensure => "installed", + before => Notify["$Sync_Point_KVM"], + } + -> + exec {"Debug_Package_virt-viewer": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_virt-viewer \n name=virt-viewer \n ensure=installed \n\" >> ${n1k_vsm::Debug_Log}", + } + + package {"Package_virt-manager": + name => "virt-manager", + ensure => "installed", + before => Notify["$Sync_Point_KVM"], + } + -> + exec {"Debug_Package_virt-manager": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_virt-manager \n name=virt-manager \n ensure=installed\n\" >> ${n1k_vsm::Debug_Log}", + } + + package {"Package_libvirt": + name => "libvirt", + ensure => "installed", + before => Notify["$Sync_Point_KVM"], + } + -> + exec {"Debug_Package_libvirt": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_libvirt \n name=libvirt \n ensure=installed\n\" >> ${n1k_vsm::Debug_Log}", + } + + package {"Package_libvirt-python": + name => "libvirt-python", + ensure => "installed", + before => Notify["$Sync_Point_KVM"], + } + -> + exec {"Debug_Package_libvirt-python": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_libvirt-python \n name=libvirt-python \n ensure=installed\n\" >> ${n1k_vsm::Debug_Log}", + } + + #package {"Package_python-virtinst": + # name => "python-virtinst", + # ensure => "installed", + # before => Notify["$Sync_Point_KVM"], + #} + #-> + #exec {"Debug_Package_python-virtinst": + # command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_python-virtinst \n name=python-virtinst \n ensure=installed \n\" >> ${n1k_vsm::Debug_Log}", + #} + + #package {"Package_genisoimage": + # name => "genisoimage", + # ensure => "installed", + # before => Notify["$Sync_Point_KVM"], + #} + #-> + #exec {"Debug_Package_genisoimage": + # command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_genisoimage \n name=genisoimage \n ensure=installed \n\" >> ${n1k_vsm::Debug_Log}", + #} + + package {"Package_ebtables": + name => "ebtables", + #ensure => "purged", + ensure => "installed", + before => Notify["$Sync_Point_KVM"], + } + -> + exec {"Debug_Package_ebtables": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_ebtables \n name=ebtables \n ensure=purged \n\" >> ${n1k_vsm::Debug_Log}", + } + + notify{"$Sync_Point_KVM":} + + service {"Service_libvirtd": + name => "libvirtd", + ensure => "running", + } + -> + exec {"Debug_Service_libvirtd": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Service_libvirtd\n name=libvirtd \n ensure=running \n\" >> ${n1k_vsm::Debug_Log}", + } + + # + # Virsh network exec configuration section + # + exec {"Exec_removenet": + command => "/usr/bin/virsh net-destroy default || /bin/true", + unless => "/usr/bin/virsh net-info default | /bin/grep -c 'Active: .* no'", + before => Notify["$Sync_Point_Virsh_Network"], + } + -> + exec {"Debug_Exec_removenet": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Exec_removenet \n command=/usr/bin/virsh net-destroy default || /bin/true \n unless=/usr/bin/virsh net-info default | /bin/grep -c 'Active: .* no'\n\" >> ${n1k_vsm::Debug_Log}", + } + + exec {"Exec_disableautostart": + command => "/usr/bin/virsh net-autostart --disable default || /bin/true", + unless => "/usr/bin/virsh net-info default | /bin/grep -c 'Autostart: .* no'", + before => Notify["$Sync_Point_Virsh_Network"], + } + -> + exec {"Debug_Exec_disableautostart": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Exec_disableautostart' \n command=/usr/bin/virsh net-autostart --disable default || /bin/true \n unless /usr/bin/virsh net-info default | grep -c 'Autostart: .* no'\" >> ${n1k_vsm::Debug_Log}", + } + + notify{"$Sync_Point_Virsh_Network":} + + package {"Package_openvswitch": + name => "openvswitch", + ensure => "installed", + } + -> + exec {"Debug_Package_openvswitch": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Package_openvswitch \n name=openvswitch \n ensure=installed\n\" >> ${n1k_vsm::Debug_Log}", + } + # + # bring up OVS and perform interface configuration + # + + service {"Service_openvswitch": + name => "openvswitch", + ensure => "running", + enable => "true", + } + -> + exec {"Debug_Service_openvswitch": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Service_openvswitch \n name=openvswitch \n ensure=running \n enable=true\n\" >> ${n1k_vsm::Debug_Log}", + } + + + exec {"Exec_AddOvsBr": + command => "/usr/bin/ovs-vsctl -- --may-exist add-br $n1k_vsm::ovsbridge", + } + -> + exec {"Debug_Exec_AddOvsBr": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Exec_AddOvsBr \n command=/usr/bin/ovs-vsctl -- --may-exist add-br $n1k_vsm::ovsbridge \n \" >> ${n1k_vsm::Debug_Log}", + } + + # + # Modify Ovs bridge inteface configuation file + # + augeas {"Augeas_modify_ifcfg-ovsbridge": + name => "$n1k_vsm::ovsbridge", + context => "/files/etc/sysconfig/network-scripts/ifcfg-$n1k_vsm::ovsbridge", + changes => [ + "set DEVICE $n1k_vsm::ovsbridge", + "set BOOTPROTO none", + "set IPADDR $n1k_vsm::nodeip", + "set NETMASK $n1k_vsm::nodenetmask", + "set ONBOOT yes", + "set TYPE OVSBridge", + "set DEVICETYPE ovs", + ], + notify => Service["Service_network"], + } + -> + exec {"Debug_Augeas_modify_ifcfg-ovsbridge": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Augeas_modify_ifcfg-$n1k_vsm::ovsbridge \n name=$n1k_vsm::ovsbridge \n context=/files/etc/sysconfig/network-scripts/ifcfg-$n1k_vsm::ovsbridge \n\" >> ${n1k_vsm::Debug_Log}", + } + + # + # Modify Physical Interface config file + # + augeas {"Augeas_modify_ifcfg-physicalinterfaceforovs": + name => "$n1k_vsm::physicalinterfaceforovs", + context => "/files/etc/sysconfig/network-scripts/ifcfg-$n1k_vsm::physicalinterfaceforovs", + changes => [ + "set ONBOOT yes", + "set BOOTPROTO none", + "set TYPE OVSPort", + "set DEVICETYPE ovs", + "set OVS_BRIDGE $n1k_vsm::ovsbridge", + "rm IPADDR", + "rm NETMASK", + ], + } + -> + exec {"Debug_Augeas_modify_ifcfg-physicalinterfaceforovs": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Augeas_modify_ifcfg-physicalinterfaceforovs \n name=$n1k_vsm::physicalinterfaceforovs \n context=/files/etc/sysconfig/network-scripts/ifcfg-$n1k_vsm::physicalinterfaceforovs\n\" >> ${n1k_vsm::Debug_Log}", + } + + $intf=$n1k_vsm::physicalinterfaceforovs + $phy_bridge="/tmp/phy_bridge" + # + # Move physical port around from host bridge if any, to ovs bridge + # + #exec {"Exec_phy_bridge": + # command => "/usr/sbin/brctl show | /bin/grep $intf | /bin/sed 's/[\t ].*//' > $phy_bridge", + #} + #-> + #exec {"Debug_Exec_phy_bridge": + # command => "${n1k_vsm::Debug_Print} \"[INFO]\n Exec_phy_bridge \n /usr/sbin/brctl show | /bin/grep $intf | /bin/sed 's/[\t ].*//' > $phy_bridge \n \" >> ${n1k_vsm::Debug_Log}", + #} + + exec {"Exec_rebridge": + #command => "/usr/bin/test -s $phy_bridge && /usr/sbin/brctl delif \$(cat $phy_bridge) $intf || /bin/true; /usr/bin/ovs-vsctl -- --may-exist add-port $n1k_vsm::ovsbridge $intf", + command => "/usr/bin/ovs-vsctl -- --may-exist add-port $n1k_vsm::ovsbridge $intf", + #notify => Service["Service_network"], + } + -> + exec {"Debug_Exec_rebridge": + #command => "${n1k_vsm::Debug_Print} \"[INFO]\n Exec_rebridge \n /usr/bin/test -s $phy_bridge && /usr/sbin/brctl delif \$(cat $phy_bridge) $intf || /bin/true; /usr/bin/ovs-vsctl -- --may-exist add-port $n1k_vsm::ovsbridge $intf; /bin/rm -f $phy_bridge \n\" >> ${n1k_vsm::Debug_Log}", + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Exec_rebridge \n /usr/bin/ovs-vsctl -- --may-exist add-port $n1k_vsm::ovsbridge $intf \n\" >> ${n1k_vsm::Debug_Log}", + } + + # + # Order enforcement logic + # + #Notify["$Sync_Point_KVM"] -> Service["Service_libvirtd"] -> Notify["$Sync_Point_Virsh_Network"] -> Package["Package_openvswitch"] -> Service["Service_openvswitch"] -> Exec["Exec_AddOvsBr"]->Augeas["Augeas_modify_ifcfg-ovsbridge"]->Augeas["Augeas_modify_ifcfg-physicalinterfaceforovs"]->Exec["Exec_phy_bridge"]->Exec["Exec_rebridge"] + Notify["$Sync_Point_KVM"] -> Service["Service_libvirtd"] -> Notify["$Sync_Point_Virsh_Network"] -> Package["Package_openvswitch"] -> Service["Service_openvswitch"] -> Exec["Exec_AddOvsBr"]->Augeas["Augeas_modify_ifcfg-ovsbridge"]->Augeas["Augeas_modify_ifcfg-physicalinterfaceforovs"]->Exec["Exec_rebridge"] + } + "Ubuntu": { + } + default: { + # + # bail out for unsupported OS + # + fail(": os[$os] is not supported") + } + } +} diff --git a/n1k-vsm/manifests/vsmprep.pp b/n1k-vsm/manifests/vsmprep.pp new file mode 100644 index 000000000..4c9ae8695 --- /dev/null +++ b/n1k-vsm/manifests/vsmprep.pp @@ -0,0 +1,162 @@ +class n1k_vsm::vsmprep { + include 'stdlib' + + # + # VSM package source parsing logic + # + $source = $n1k_vsm::n1kv_source + + $source_method = regsubst($source, "^(.+):.*", '\1') + $dest = inline_template('<%= File.basename(source) %>') + + + $VSM_Bin_Prepare_Sync_Point="##VSM_BIN_PREPARE_SYNC_POINT" + $VSM_Spool_Dir="/var/spool/vsm" + $VSM_RPM_Install_Dir="/opt/cisco/vsm" + $VSM_Repackage_Script_Name="repackiso.py" + $VSM_Repackage_Script="/tmp/$VSM_Repackage_Script_Name" + $VSM_DEST="$VSM_Spool_Dir/$dest" + $VSM_PKG_NAME="nexus-1000v-vsm" + $VSM_ISO="vsm.iso" + + # + # prepare vsm spool folder + # + file {"File_VSM_Spool_Dir": + path => "$VSM_Spool_Dir", + ensure => "directory", + owner => "root", + group => "root", + mode => "664", + } + -> + exec {"Debug_File_VSM_Spool_Dir": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n File_VSM_Spool_Dir\n path=$VSM_Spool_Dir \n ensure=directory \n owner=root \n group=root \n mode=664 \n\" >> ${n1k_vsm::Debug_Log}", + } + + + case "$source_method" { + "http": { + yumrepo {"http-cisco-foreman": + baseurl => "$n1k_vsm::n1kv_source", + descr => "Internal repo for Foreman", + enabled => "1", + gpgcheck => "1", + proxy => "_none_", + gpgkey => "${n1k_vsm::n1kv_source}/RPM-GPG-KEY", + } + -> + package {"Package_VSM": + name => "$VSM_PKG_NAME", + ensure => "${n1k_vsm::n1kv_version}", + } + -> + exec {"Copy_VSM": + command => "/bin/cp $VSM_RPM_Install_Dir/*.iso $VSM_Spool_Dir/$VSM_ISO", + before => Notify["$VSM_Bin_Prepare_Sync_Point"], + } + -> + exec {"Debug-http-cisco-os and Package_VSM": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Debug-http-cisco-os and Package_VSM \n baseurl=$n1k_vsm::n1kv_source \n descr=>Internal repo for Foreman \n enabled = 1 \n gpgcheck=1 \n gpgkey => $n1kv_source::n1kv_source/RPM-GPG-KEY\n\" >> ${n1k_vsm::Debug_Log}", + } + } + + "ftp": { + package {"ftp": + name => "ftp", + ensure => "installed", + } + -> + yumrepo {"ftp-cisco-foreman": + baseurl => "$n1k_vsm::n1kv_source", + descr => "Internal repo for Foreman", + enabled => "1", + gpgcheck => "1", + proxy => "_none_", + gpgkey => "${n1k_vsm::n1kv_source}/RPM-GPG-KEY", + } + -> + package {"Package_VSM": + name => "$VSM_PKG_NAME", + ensure => "${n1k_vsm::n1kv_version}", + } + -> + exec {"Copy_VSM": + command => "/bin/cp $VSM_RPM_Install_Dir/*.iso $VSM_Spool_Dir/$VSM_ISO", + before => Notify["$VSM_Bin_Prepare_Sync_Point"], + } + -> + exec {"Debug-ftp-cisco-os and Package_VSM": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Debug-ftp-cisco-os and Package_VSM \n baseurl=$n1k_vsm::n1kv_source \n descr=>Internal repo for Foreman \n enabled = 1 \n gpgcheck=1 \n gpgkey => $n1kv_source::n1kv_source/RPM-GPG-KEY\n\" >> ${n1k_vsm::Debug_Log}", + } + + } + "puppet": { + # + # make sure the file does not exist + # + exec {"File_VSM_Bin_Remove": + command => "/bin/rm -f $VSM_DEST || /bin/true", + before => Notify["$VSM_Bin_Prepare_Sync_Point"], + } + -> + file {"File_VSM_Bin_Prepare": + path => "$VSM_DEST", + ensure => "present", + owner => "root", + group => "root", + mode => "664", + source => "$n1k_vsm::n1kv_source", + before => Notify["$VSM_Bin_Prepare_Sync_Point"], + } + -> + exec {"Exec_RPM_TO_ISO": + # + # If it's an RPM, we do a local rpm installation ..." + # + command => "/bin/rpm -i --force $VSM_DEST && /bin/cp $VSM_RPM_Install_Dir/*.iso $VSM_Spool_Dir/$VSM_ISO", + unless => "/usr/bin/file $VSM_DEST | /bin/grep -c ' ISO '", + before => Notify["$VSM_Bin_Prepare_Sync_Point"], + } + -> + exec {"Debug_File_VSM_Bin_Prepare_Exec_RPM_TO_ISO": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Debug_File_VSM_Bin_Prepare_Exec_RPM_TO_ISO \n path=$VSM_DEST \n ensure=directory \n owner=root\n group=root\n mode=664\n source=$n1k_vsm::n1kv_source\n \" >> ${n1k_vsm::Debug_Log}", + } + } + default: { + fail(": Unknown sourcing method [$source_method] is not supported") + } + } + + notify {"$VSM_Bin_Prepare_Sync_Point":} + + # + # copy repackiso.py to local place + # + file {"File_VSM_Repackage_Script_Name": + path => "$VSM_Repackage_Script", + ensure => "present", + owner => "root", + group => "root", + mode => "774", + source => "puppet:///modules/n1k_vsm/$VSM_Repackage_Script_Name", + } + -> + exec {"Debug_File_VSM_Repackage_Script_Name": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Debug_VSM_Repackage_Script_Name \n path=$VSM_Repackage_Script \n ensure=present \n owner=root \n group=root \n mode=774\n source=puppet:///modules/n1k_vsm/$VSM_REPACKAGE_SCRIPT_NAME \n\" >> ${n1k_vsm::Debug_Log}", + } + + # + # Now generate ovf xml file and repackage the iso + # + exec {"Exec_VSM_Repackage_Script_Name": + command => "${VSM_Repackage_Script} -i$VSM_Spool_Dir/$VSM_ISO -d${n1k_vsm::domainid} -n${n1k_vsm::vsmname} -m${n1k_vsm::mgmtip} -s${n1k_vsm::mgmtnetmask} -g${n1k_vsm::mgmtgateway} -p${n1k_vsm::adminpasswd} -r${n1k_vsm::role} -f${VSM_Spool_Dir}/${n1k_vsm::role}_repacked.iso >> ${n1k_vsm::Debug_Log}", + } + -> + exec {"Debug_Exec_VSM_Repackage_Script_Name": + command => "${n1k_vsm::Debug_Print} \"[INFO]\n Exec_VSM_Repackage_Script_Name\n command=$VSM_Repackage_Script -i$VSM_ISO -d${n1k_vsm::domainid} -n${n1k_vsm::vsmname} -m${n1k_vsm::mgmtip} -s${n1k_vsm::mgmtnetmask} -g${n1k_vsm::mgmtgateway} -p${n1k_vsm::adminpasswd} -r${n1k_vsm::role} -f${VSM_Spool_Dir}/${n1k_vsm::role}_repacked.iso \n\" >> ${n1k_vsm::Debug_Log}" + } + + File["File_VSM_Spool_Dir"]-> Notify["$VSM_Bin_Prepare_Sync_Point"]->File["File_VSM_Repackage_Script_Name"]->Exec["Exec_VSM_Repackage_Script_Name"] + +} diff --git a/n1k-vsm/templates/vsm_vm.xml.erb b/n1k-vsm/templates/vsm_vm.xml.erb new file mode 100644 index 000000000..74d017eff --- /dev/null +++ b/n1k-vsm/templates/vsm_vm.xml.erb @@ -0,0 +1,86 @@ + + <%= scope.lookupvar('n1k_vsm::vsmname') %> + <%= scope.lookupvar('n1k_vsm::memory') %> + <%= scope.lookupvar('n1k_vsm::vcpu') %> + + + hvm + + + + + + + + destroy + restart + restart + + + /usr/libexec/qemu-kvm + + + '/> + + + + + + '/> + + + + + + +
+ + + + + + + + +
+ + + + + + + + +
+ + + + + + + + +
+ + + + + + + + +